using System; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Security.Principal; using Lithnet.CredentialProvider.Interop; using Microsoft.Win32.SafeHandles; using Windows.Win32.Foundation; using Windows.Win32.System.Threading; using NativeMethods = Windows.Win32.PInvoke; namespace Lithnet.CredentialProvider { /// /// ConsentUIData is an abstract base class that represents all the different types of data structures that can be passed to the ConsentUI process for a UAC elevation prompt. /// The static members of the class can be used to retrieve the data structure passed to the ConsentUI process, or to determine if the current process is the ConsentUI process. /// The caller will be provided with one of the concrete implementations of this class, depending on the type of data structure that was passed to the ConsentUI process. /// Use the property to determine the type of data structure and cast it to one of the concrete implementations. /// public abstract class ConsentUIData { private static bool? isConsentUI; private static ConsentUICommandLineArgs commandLineArgs; private protected ConsentUIStructureHeader header; private readonly byte[] rawData; /// /// Gets a value indicating the type of ConsentUI data structure /// public ConsentUIType Type => this.header.Type; /// /// Gets a value indicating the consent prompt type /// public ConsentUIPromptType PromptType => this.header.PromptType; /// /// Gets a handle to the Window that was responsible for invoking the ConsentUI prompt /// public IntPtr HWnd => this.header.hWindow; /// /// Gets the method that ConsentUI has been told to fetch approval. /// In the case where a Credential Provider is initialised, this should always be `Credentials`. /// public ConsentUIElevationType ElevationType => this.header.ElevationType; /// /// A series of flags that AppInfo passes to ConsentUI to signifiy actions that need to /// take place on the UI side. /// This includes specifics around the UI that should be presented & signature verification settings. /// public ConsentUIFlags Flags => this.header.Flags; /// /// Gets the ID of the session where the ConsentUI prompt was originally invoked /// public int SessionId => this.header.sessionId; private protected ConsentUIData(IntPtr pData, int expectedSize) { this.rawData = GetRawBytes(pData, expectedSize); this.header = Marshal.PtrToStructure(pData); if (this.header.Size != expectedSize) { throw new InvalidDataException($"The size of the data structure {this.header.Size} does not match the expected size {expectedSize}"); } } /// /// Gets the Windows Identity from the original caller requesting elevation /// /// A WindowsIdentity object that represents the user requesting elevation /// Thrown when the user's token could not be obtained from the session information public WindowsIdentity GetWindowsIdentity() { var duplicatedToken = DuplicateHandleInternal(this.header.hToken); return new WindowsIdentity(duplicatedToken.DangerousGetHandle()); } /// /// Gets a raw byte array representing the ConsentUI data structure /// /// A byte array public byte[] GetRawData() { return this.rawData; } /// /// Gets a string value that is packed at the end of the data structure if the offset is valid /// /// A pointer to the start of the data structure /// The position from the start of the data structure where the string starts /// A string containing all characters from the given offset up to the first null character found private protected string GetStringValueIfValid(IntPtr pData, int offset) { if (offset > 0) { this.ThrowOnInvalidOffset(offset); return Marshal.PtrToStringUni(IntPtr.Add(pData, offset)); } return null; } /// /// Throws an exception if the given offset is greater than the size of the data structure /// /// The value of the offset /// Thrown when the value of the pointer is greater than the expected data size private protected void ThrowOnInvalidOffset(int value) { if (value >= this.header.Size) { throw new InvalidDataException($"Offset value {value} is greater than the expected data size {this.header.Size}"); } } /// /// Creates a ConsentUIData object from a previously-obtained raw byte representation /// /// The raw bytes of a supported ConsentUI data structure /// A ConsentUIData object public static ConsentUIData GetConsentUIData(byte[] consentUIDataStructure) { SafeHGlobalHandle pData = SafeHGlobalHandle.AllocHGlobal(consentUIDataStructure.Length); Marshal.Copy(consentUIDataStructure, 0, pData.ToIntPtr(), consentUIDataStructure.Length); return CreateInstance(pData.ToIntPtr(), consentUIDataStructure.Length); } /// /// Gets the data structure passed to the Consent UI process /// /// A ConsentUIData object public static ConsentUIData GetConsentUIData() { var pData = GetConsentUIData(out int structSize); return ConsentUIData.CreateInstance(pData.ToIntPtr(), structSize); } /// /// Gets a value indicating whether the current process is consent.exe, indicating that the provider is running inside an elevated UAC prompt /// /// public static bool IsConsentUIParent() { if (isConsentUI == null) { var consentPath = Environment.ExpandEnvironmentVariables("%systemroot%\\system32\\consent.exe"); var process = Process.GetCurrentProcess(); var callingProcess = process.MainModule?.FileName; isConsentUI = string.Equals(callingProcess, consentPath, StringComparison.OrdinalIgnoreCase); } return isConsentUI.Value; } /// /// Gets the raw bytes of the ConsentUI data structure /// /// A byte array public static byte[] GetConsentUIDataRawBytes() { var pData = GetConsentUIData(out int structSize); return GetRawBytes(pData.ToIntPtr(), structSize); } /// /// Parses the command line of the consent.exe process to retrieve the data structure passed to it /// /// Returns the size of the data structure as reported in the command line arguments /// A pointer to the newly created copy of the data structure /// Thrown when either consent.exe is not the parent process /// Throw when the arguments passed to consent.exe are invalid private static SafeHGlobalHandle GetConsentUIData(out int size) { if (!IsConsentUIParent()) { throw new InvalidOperationException("The consent UI data can only be retrieved when consent.exe is the parent process"); } commandLineArgs ??= GetConsentUICommandLineArgs(); size = commandLineArgs.Size; return ReadMemoryFromProcess(commandLineArgs.AppInfoProcessId, commandLineArgs.Address, commandLineArgs.Size); } /// /// Extracts the command line arguments passed to the consent.exe process /// /// A ConsentUICommandLineArgs object containing the arguments parsed from the command line /// Thrown when the arguments passed to consent.exe cannot be parsed or are of the incorrect number private static ConsentUICommandLineArgs GetConsentUICommandLineArgs() { var args = Environment.GetCommandLineArgs(); if (args.Length != 4) { throw new ArgumentException($"Unable to parse command line of consent.exe. The number of elements was incorrect\r\n{string.Join("\r\n", args)}"); } if (!uint.TryParse(args[1], out var appInfoPid)) { throw new ArgumentException($"Unable to parse command line of consent.exe. The expected first element was not an integer\r\n{string.Join("\r\n", args)}"); } if (!int.TryParse(args[2], out var size)) { throw new ArgumentException($"Unable to parse command line of consent.exe. The expected second element was not an integer\r\n{string.Join("\r\n", args)}"); } if (!long.TryParse(args[3], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var address)) { throw new ArgumentException($"Unable to parse command line of consent.exe. The expected third element was not an integer\r\n{string.Join("\r\n", args)}"); } return new ConsentUICommandLineArgs { Address = address, AppInfoProcessId = appInfoPid, Size = size, }; } /// /// Copies the memory from a raw pointer into a managed byte array /// /// The pointer where the data copy must start /// The number of bytes to copy /// A copy of the raw memory returned as a managed byte array private static byte[] GetRawBytes(IntPtr pData, int size) { byte[] dataForExport = new byte[size]; Marshal.Copy(pData, dataForExport, 0, size); return dataForExport; } /// /// Reads the memory from a specified process /// /// The ID of the process /// The memory address to read /// The size of the data at the specified memory address /// A handle to a copy of the process memory /// Thrown when the process could not be opened or the memory address could not be read /// Thrown when the size of the copied structure did not equal the expected size as passed to the method private static SafeHGlobalHandle ReadMemoryFromProcess(uint processId, long address, int size) { SafeHGlobalHandle pData = SafeHGlobalHandle.AllocHGlobal(size); var pAddress = new IntPtr(address); SafeFileHandle hProcess = OpenProcessHandle(processId, PROCESS_ACCESS_RIGHTS.PROCESS_VM_READ); unsafe { nuint numberOfBytesRead = 0; if (!NativeMethods.ReadProcessMemory(hProcess, pAddress.ToPointer(), pData.ToIntPtr().ToPointer(), (nuint)size, &numberOfBytesRead)) { int error = Marshal.GetLastWin32Error(); throw new Win32Exception(error, $"Unable to read memory from process {processId}"); } if (numberOfBytesRead != (nuint)size) { throw new InvalidDataException($"Bytes read from memory {numberOfBytesRead} was not the expected structure size {size}"); } } return pData; } /// /// Opens a native handle to a process /// /// The ID of the process /// The requested access rights /// A safe handle to the process /// Thrown when the process handle could not be obtained private static SafeFileHandle OpenProcessHandle(uint processId, PROCESS_ACCESS_RIGHTS rights) { var hProcess = NativeMethods.OpenProcess_SafeHandle(rights, false, processId); if (hProcess.IsInvalid) { int error = Marshal.GetLastWin32Error(); throw new Win32Exception(error, $"Unable to open process {processId}"); } return hProcess; } /// /// Creates an instance of the appropriate subclass of ConsentUIData by reading the type from the data structure /// /// A pointer to the data structure /// The expected size of the data structure /// A ConsentUIData object /// Thrown when the size of the expected data structure does not match the size reported in the structure itself private static ConsentUIData CreateInstance(IntPtr pData, int expectedSize) { var sizeReportedInStructure = Marshal.ReadInt32(pData, 0); if (sizeReportedInStructure != expectedSize) { throw new InvalidDataException($"The expected size {expectedSize} did not match the size reported by the structure {sizeReportedInStructure}"); } var type = (ConsentUIType)Marshal.ReadInt32(pData, 4); return type switch { ConsentUIType.Exe => new ConsentUIDataExe(pData, sizeReportedInStructure), ConsentUIType.Msi => new ConsentUIDataMsi(pData, sizeReportedInStructure), ConsentUIType.Com => new ConsentUIDataCom(pData, sizeReportedInStructure), ConsentUIType.Msix => new ConsentUIDataMsix(pData, sizeReportedInStructure), ConsentUIType.ActiveX => new ConsentUIDataActiveX(pData, sizeReportedInStructure), ConsentUIType.CredCollect => new ConsentUIDataCredCollect(pData, sizeReportedInStructure), _ => throw new InvalidDataException("The ConsentUI data structure was for an unknown type"), }; } /// /// Duplicates a handle passed in from the AppInfo service /// /// The handle to duplicate /// A duplicated reference to the handle /// Thrown when the handle could not be duplicated protected private static SafeHandle DuplicateHandleInternal(IntPtr handle) { commandLineArgs ??= GetConsentUICommandLineArgs(); var processHandle = OpenProcessHandle(commandLineArgs.AppInfoProcessId, PROCESS_ACCESS_RIGHTS.PROCESS_DUP_HANDLE); SafeFileHandle t = new(handle, false); if (!NativeMethods.DuplicateHandle(processHandle, t, Process.GetCurrentProcess().SafeHandle, out var duplicatedToken, 0, false, DUPLICATE_HANDLE_OPTIONS.DUPLICATE_SAME_ACCESS)) { throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to duplicate the handle"); } return duplicatedToken; } } }