diff --git a/src/Lithnet.CredentialProvider/ConsentUI/ConsentUICommandLineArgs.cs b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUICommandLineArgs.cs new file mode 100644 index 0000000..dfee9ee --- /dev/null +++ b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUICommandLineArgs.cs @@ -0,0 +1,11 @@ +namespace Lithnet.CredentialProvider +{ + internal class ConsentUICommandLineArgs + { + public uint AppInfoProcessId { get; set; } + + public int Size { get; set; } + + public long Address { get; set; } + } +} \ No newline at end of file diff --git a/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIData.cs b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIData.cs new file mode 100644 index 0000000..6c56ae8 --- /dev/null +++ b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIData.cs @@ -0,0 +1,337 @@ +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 int PromptType => this.header.PromptType; + + /// + /// Gets a handle to the Window that was responsible for invoking the ConsentUI prompt + /// + public IntPtr HWnd => this.header.hWnd; + + public ElevationType ElevationType => this.header.elevationType; + + /// + /// 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; + } + } +} \ No newline at end of file diff --git a/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataActiveX.cs b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataActiveX.cs new file mode 100644 index 0000000..c19dc3a --- /dev/null +++ b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataActiveX.cs @@ -0,0 +1,19 @@ +using System; + +namespace Lithnet.CredentialProvider +{ + /// + /// Represents the data structure passed to consent UI when a user is trying to install an ActiveX control + /// + /// This data structure is currently unknown and only the common header values are present + public class ConsentUIDataActiveX : ConsentUIData + { + internal ConsentUIDataActiveX(IntPtr pData, int expectedSize) : base(pData, expectedSize) + { + if (this.header.Type != ConsentUIType.ActiveX) + { + throw new InvalidOperationException("The data structure is not of type ActiveX"); + } + } + } +} diff --git a/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataCom.cs b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataCom.cs new file mode 100644 index 0000000..c596f0e --- /dev/null +++ b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataCom.cs @@ -0,0 +1,53 @@ +using System; +using System.Runtime.InteropServices; +using Lithnet.CredentialProvider.Interop; + +namespace Lithnet.CredentialProvider +{ + /// + /// Represents the data structure passed to consent UI when a user is trying to elevate a COM component. This is typically seen when a user presses a 'shield' icon in something like the file security dialog to elevate permissions. + /// + public class ConsentUIDataCom : ConsentUIData + { + /// + /// The path to the COM component that is requesting elevation + /// + public string ComComponentPath { get; } + + /// + /// The resource path to the image to display in consent UI + /// + public string ImageResourcePath { get; } + + /// + /// The path to the process that is hosting the COM component + /// + public string ProcessPath { get; } + + /// + /// A user friendly description of the type of operation that will be performed by the elevation + /// + public string OperationType { get; } + + /// + /// The CLSID of the COM component + /// + public Guid ClsId { get; } + + internal ConsentUIDataCom(IntPtr pData, int expectedSize) : base(pData, expectedSize) + { + if (this.header.Type != ConsentUIType.Com) + { + throw new InvalidOperationException("The data structure is not of type COM"); + } + + var s = Marshal.PtrToStructure(pData); + + this.ComComponentPath = this.GetStringValueIfValid(pData, (int)s.oComComponentPath); + this.ImageResourcePath = this.GetStringValueIfValid(pData, (int)s.oImageResourcePath); + this.ProcessPath = this.GetStringValueIfValid(pData, (int)s.oProcessPath); + this.OperationType = this.GetStringValueIfValid(pData, (int)s.oOperationType); + this.ClsId = s.Clsid; + } + } +} diff --git a/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataCredCollect.cs b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataCredCollect.cs new file mode 100644 index 0000000..a8554b6 --- /dev/null +++ b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataCredCollect.cs @@ -0,0 +1,19 @@ +using System; + +namespace Lithnet.CredentialProvider +{ + /// + /// Represents the data structure passed to consent UI in a yet unknown scenario + /// + /// This data structure is currently unknown and only the common header values are present + public class ConsentUIDataCredCollect : ConsentUIData + { + internal ConsentUIDataCredCollect(IntPtr pData, int expectedSize) : base(pData, expectedSize) + { + if (this.header.Type != ConsentUIType.CredCollect) + { + throw new InvalidOperationException("The data structure is not of type CredCollect"); + } + } + } +} diff --git a/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataExe.cs b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataExe.cs new file mode 100644 index 0000000..4d74baa --- /dev/null +++ b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataExe.cs @@ -0,0 +1,59 @@ +using System; +using System.Runtime.InteropServices; +using Lithnet.CredentialProvider.Interop; + +namespace Lithnet.CredentialProvider +{ + /// + /// Represents the data structure passed to consent UI when a user is trying to elevate an executable + /// + public class ConsentUIDataExe : ConsentUIData + { + private IntPtr hFile; + + /// + /// The path to the process that the user has requested to be launched as an administrator + /// + public string ExecutablePath { get; } + + /// + /// A currently unknown value. In most cases it seems to be the same as + /// + public string Unknown1 { get; } + + /// + /// The full command line, including arguments that will be used to launch the executable + /// + public string CommandLine { get; } + + /// + /// A currently unknown parameter + /// + public string Unknown2 { get; } + + /// + /// Gets a handle to the executable that the user has requested to be launched as an administrator + /// + /// A handle to the executable + public SafeHandle GetExecutableHandle() + { + return DuplicateHandleInternal(this.hFile); + } + + internal ConsentUIDataExe(IntPtr pData, int expectedSize) : base(pData, expectedSize) + { + if (this.header.Type != ConsentUIType.Exe) + { + throw new InvalidOperationException("The data structure is not of type EXE"); + } + + var s = Marshal.PtrToStructure(pData); + + this.hFile = s.hFile; + this.ExecutablePath = this.GetStringValueIfValid(pData, (int)s.oExecutablePath1); + this.Unknown1 = this.GetStringValueIfValid(pData, (int)s.oExecutablePath2); + this.CommandLine = this.GetStringValueIfValid(pData, (int)s.oCommandLine); + this.Unknown2 = this.GetStringValueIfValid(pData, (int)s.oUnknown0); + } + } +} diff --git a/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataMsi.cs b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataMsi.cs new file mode 100644 index 0000000..930412b --- /dev/null +++ b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataMsi.cs @@ -0,0 +1,71 @@ +using System; +using System.Runtime.InteropServices; +using Lithnet.CredentialProvider.Interop; + +namespace Lithnet.CredentialProvider +{ + /// + /// Represents the data structure passed to consent UI when a user is trying to elevate an MSI installer + /// + public class ConsentUIDataMsi : ConsentUIData + { + /// + /// The name of the product being installed + /// + public string ProductName { get; } + + /// + /// The version of the product being installed + /// + public string Version { get; } + + /// + /// The locale of the product being installed + /// + public string Locale { get; } + + /// + /// The publisher of the product being installed + /// + public string Publisher { get; } + + /// + /// The path to the MSI installer + /// + public string ExecutionPath { get; } + + /// + /// The path to the original MSI file launched by the user + /// + public string OriginalMsi { get; } + + /// + /// A currently unknown parameter + /// + public string Unknown1 { get; } + + /// + /// A currently unknown parameter + /// + public string Unknown2 { get; } + + internal ConsentUIDataMsi(IntPtr pData, int expectedSize) : base(pData, expectedSize) + { + if (this.header.Type != ConsentUIType.Msi) + { + throw new InvalidOperationException("The data structure is not of type MSI"); + } + + var s = Marshal.PtrToStructure(pData); + + this.ProductName = this.GetStringValueIfValid(pData, (int)s.oProductName); + this.Version = this.GetStringValueIfValid(pData, (int)s.oVersion); + this.Locale = this.GetStringValueIfValid(pData, (int)s.oLocale); + this.Publisher = this.GetStringValueIfValid(pData, (int)s.oPublisher); + this.ExecutionPath = this.GetStringValueIfValid(pData, (int)s.oExecutionPath); + this.OriginalMsi = this.GetStringValueIfValid(pData, (int)s.oOriginalMsi); + this.Unknown1 = this.GetStringValueIfValid(pData, (int)s.oUnknown1); + this.Unknown2 = this.GetStringValueIfValid(pData, (int)s.oUnknown2); + } + } +} diff --git a/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataMsix.cs b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataMsix.cs new file mode 100644 index 0000000..cbced84 --- /dev/null +++ b/src/Lithnet.CredentialProvider/ConsentUI/ConsentUIDataMsix.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.Interop +{ + /// + /// Represents the data structure passed to consent UI when a user is trying to elevate an MSIX package + /// + public class ConsentUIDataMsix : ConsentUIData + { + /// + /// The path to the package being installed + /// + public string ExecutablePath { get; } + + /// + /// The name of the package being installed + /// + public string PackageName { get; } + + /// + /// The full command line, including any arguments used to launch the installer + /// + public string CommandLine { get; } + + /// + /// A currently unknown parameter + /// + public string OtherName { get; } + + internal ConsentUIDataMsix(IntPtr pData, int expectedSize) : base(pData, expectedSize) + { + if (this.header.Type != ConsentUIType.Msix) + { + throw new InvalidOperationException("The data structure is not of type MSIX"); + } + + var s = Marshal.PtrToStructure(pData); + + this.ExecutablePath = this.GetStringValueIfValid(pData, (int)s.oExecutablePath); + this.PackageName = this.GetStringValueIfValid(pData, (int)s.oPackageName); + this.CommandLine = this.GetStringValueIfValid(pData, (int)s.oCommandLine); + this.OtherName = this.GetStringValueIfValid(pData, (int)s.oOtherName); + } + } +} diff --git a/src/Lithnet.CredentialProvider/ConsentUI/ElevationType.cs b/src/Lithnet.CredentialProvider/ConsentUI/ElevationType.cs new file mode 100644 index 0000000..39770de --- /dev/null +++ b/src/Lithnet.CredentialProvider/ConsentUI/ElevationType.cs @@ -0,0 +1,10 @@ +namespace Lithnet.CredentialProvider +{ + public enum ElevationType + { + Unknown1 = 0, + Unknown2 = 1, + Consent = 2, + Credentials = 3 + } +} \ No newline at end of file diff --git a/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-com-x64.dat b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-com-x64.dat new file mode 100644 index 0000000..289236b Binary files /dev/null and b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-com-x64.dat differ diff --git a/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-com-x86.dat b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-com-x86.dat new file mode 100644 index 0000000..5097964 Binary files /dev/null and b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-com-x86.dat differ diff --git a/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-exe-x64.dat b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-exe-x64.dat new file mode 100644 index 0000000..17c483f Binary files /dev/null and b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-exe-x64.dat differ diff --git a/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-exe-x86.dat b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-exe-x86.dat new file mode 100644 index 0000000..0ef9b15 Binary files /dev/null and b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-exe-x86.dat differ diff --git a/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msi-x64.dat b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msi-x64.dat new file mode 100644 index 0000000..4087acf Binary files /dev/null and b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msi-x64.dat differ diff --git a/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msi-x86.dat b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msi-x86.dat new file mode 100644 index 0000000..9c63366 Binary files /dev/null and b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msi-x86.dat differ diff --git a/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msix-x64.dat b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msix-x64.dat new file mode 100644 index 0000000..2a4c6c0 Binary files /dev/null and b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msix-x64.dat differ diff --git a/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msix-x86.dat b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msix-x86.dat new file mode 100644 index 0000000..b1e69c2 Binary files /dev/null and b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-msix-x86.dat differ diff --git a/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-winsec-x86.dat b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-winsec-x86.dat new file mode 100644 index 0000000..311afd6 Binary files /dev/null and b/src/Lithnet.CredentialProvider/ConsentUI/Samples/dump-consent-winsec-x86.dat differ diff --git a/src/Lithnet.CredentialProvider/CredentialProviderBase.cs b/src/Lithnet.CredentialProvider/CredentialProviderBase.cs index 86cda32..605c31b 100644 --- a/src/Lithnet.CredentialProvider/CredentialProviderBase.cs +++ b/src/Lithnet.CredentialProvider/CredentialProviderBase.cs @@ -51,6 +51,16 @@ namespace Lithnet.CredentialProvider /// public IReadOnlyList Tiles { get; private set; } + /// + /// Gets a value that indicates if this credential provider is loaded by Logon UI + /// + public bool IsLogonUI => this.UsageScenario == UsageScenario.Logon; + + /// + /// Gets a value that indicates if the credential provider is loaded by Consent UI (eg UAC prompt) + /// + public bool IsConsentUI => this.UsageScenario == UsageScenario.CredUI && ConsentUIData.IsConsentUIParent(); + /// /// Provides access to the serialized input data provided by CredUI /// diff --git a/src/Lithnet.CredentialProvider/Enums/ConsentUIType.cs b/src/Lithnet.CredentialProvider/Enums/ConsentUIType.cs new file mode 100644 index 0000000..91446e7 --- /dev/null +++ b/src/Lithnet.CredentialProvider/Enums/ConsentUIType.cs @@ -0,0 +1,12 @@ +namespace Lithnet.CredentialProvider +{ + public enum ConsentUIType + { + Exe = 0, + Com = 1, + Msi = 2, + ActiveX = 3, + CredCollect = 4, + Msix = 5 + } +} diff --git a/src/Lithnet.CredentialProvider/Interop/SafeHGlobalHandle.cs b/src/Lithnet.CredentialProvider/Interop/SafeHGlobalHandle.cs new file mode 100644 index 0000000..7650635 --- /dev/null +++ b/src/Lithnet.CredentialProvider/Interop/SafeHGlobalHandle.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.Interop +{ + internal sealed class SafeHGlobalHandle : IDisposable + { + /// + /// Unmanaged pointer wrapped by this object + /// + IntPtr pointer; + + SafeHGlobalHandle() + { + this.pointer = IntPtr.Zero; + } + + SafeHGlobalHandle(IntPtr handle) + { + this.pointer = handle; + } + + ~SafeHGlobalHandle() + { + this.Dispose(); + } + + public static SafeHGlobalHandle InvalidHandle => new SafeHGlobalHandle(IntPtr.Zero); + + /// + /// Operator to obtain the unmanaged pointer wrapped by the object. Note + /// that the returned pointer is only valid for the lifetime of this + /// object. + /// + /// Unmanaged pointer wrapped by the object + public IntPtr ToIntPtr() + { + return this.pointer; + } + + public void Dispose() + { + if (this.pointer != IntPtr.Zero) + { + Marshal.FreeHGlobal(this.pointer); + this.pointer = IntPtr.Zero; + } + + GC.SuppressFinalize(this); + } + + public static SafeHGlobalHandle AllocHGlobal(int cb) + { + if (cb < 0) + { + throw new ArgumentOutOfRangeException(nameof(cb), "The value of this argument must be non-negative"); + } + + SafeHGlobalHandle result = new SafeHGlobalHandle(); + result.pointer = Marshal.AllocHGlobal(cb); + return result; + } + } +} \ No newline at end of file diff --git a/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureCom.cs b/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureCom.cs new file mode 100644 index 0000000..24a35a3 --- /dev/null +++ b/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureCom.cs @@ -0,0 +1,27 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.Interop +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct ConsentUIStructureCom + { + public ConsentUIStructureHeader Header; + + // 64 + + public IntPtr oOperationType; // 8 + public IntPtr oComComponentPath; // 8 + + // 64 + 16 == 80 + + public IntPtr oImageResourcePath; // 8 + public IntPtr oProcessPath; // 8 + + // 80 + 16 == 96 + + public Guid Clsid; // 16 + + // 96 + 16 == 112 + } +} diff --git a/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureExe.cs b/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureExe.cs new file mode 100644 index 0000000..ed69888 --- /dev/null +++ b/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureExe.cs @@ -0,0 +1,29 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.Interop +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct ConsentUIStructureExe + { + public ConsentUIStructureHeader Header; + + // 64 + + public IntPtr hFile; // 8 + public IntPtr oExecutablePath1; // 8 + + // 64 + 16 = 80 + + public IntPtr oExecutablePath2; // 8 + public IntPtr oCommandLine; // 8 + + // 80 + 16 == 96 + + public IntPtr oUnknown0; // 8 + public int ProcessId; // 4 + // Padding (x64) // 4 + + // 96 + 12 (+ 4) = 112 + } +} diff --git a/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureHeader.cs b/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureHeader.cs new file mode 100644 index 0000000..e84af60 --- /dev/null +++ b/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureHeader.cs @@ -0,0 +1,33 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.Interop +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct ConsentUIStructureHeader + { + public int Size; // 4 + public ConsentUIType Type; // 4 + public int PromptType; // 4 + // padding on x64 - 4 + + // 16 + + public IntPtr hWnd; // 8 + public IntPtr hToken; // 8 + + // 32 + + public ElevationType elevationType; // 4 + public int sessionId; // 4 + public IntPtr hMutex; // 8 + + // 48+ + + public int unknownFlags1; // 4 + public int unknownFlags2; // 4 + public IntPtr pReturnAddress; // 8 + + // 64 + } +} diff --git a/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureMsi.cs b/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureMsi.cs new file mode 100644 index 0000000..85b7ab3 --- /dev/null +++ b/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureMsi.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.Interop +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct ConsentUIStructureMsi + { + public ConsentUIStructureHeader Header; + + // 64 + + public IntPtr hUnknown1; // 8 + public IntPtr oProductName; // 8 + + // 64 + 16 == 80 + + public IntPtr oVersion; // 8 + public IntPtr oLocale; // 8 + + // 80 + 16 == 96 + + public IntPtr oPublisher; // 8 + public IntPtr oExecutionPath; // 8 + + // 96 + 16 == 112 + + public IntPtr oOriginalMsi; // 8 + public IntPtr hUnknown2; // 8 + + // 96 + 16 == 128 + + public IntPtr oUnknown1; // 8 + public IntPtr oUnknown2; // 8 + + // 128 + 16 == 144 + } +} diff --git a/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureMsix.cs b/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureMsix.cs new file mode 100644 index 0000000..456624a --- /dev/null +++ b/src/Lithnet.CredentialProvider/Interop/Structs/ConsentUIStructureMsix.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.CredentialProvider.Interop +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct ConsentUIStructureMsix + { + public ConsentUIStructureHeader Header; + + // 64 + + public IntPtr oExecutablePath; // 8 + public IntPtr oCommandLine; // 8 + + // 64 + 16 == 80 + + public IntPtr oPackageName; // 8 + public IntPtr oOtherName; // 8 + + // 80 + 16 == 96 + + public int ProcessId; // 4 + // Padding (x64) // 4 + + // 96 + 4 + 4 = 104 + } +} diff --git a/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj b/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj index b6161f4..34d41dc 100644 --- a/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj +++ b/src/Lithnet.CredentialProvider/Lithnet.CredentialProvider.csproj @@ -17,8 +17,8 @@ Lithnet Copyright 2023 Lithnet Pty Ltd Lithnet Windows Credential Provider - 1.0.0 - alpha1 + 1.1.0 + beta1 Lithnet true true @@ -43,6 +43,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + diff --git a/src/Lithnet.CredentialProvider/NativeMethods.json b/src/Lithnet.CredentialProvider/NativeMethods.json new file mode 100644 index 0000000..6d04a57 --- /dev/null +++ b/src/Lithnet.CredentialProvider/NativeMethods.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json" +} \ No newline at end of file diff --git a/src/Lithnet.CredentialProvider/NativeMethods.txt b/src/Lithnet.CredentialProvider/NativeMethods.txt new file mode 100644 index 0000000..9b8730e --- /dev/null +++ b/src/Lithnet.CredentialProvider/NativeMethods.txt @@ -0,0 +1,7 @@ +ReadProcessMemory +OpenProcess +WTSQueryUserToken +GetTokenInformation +CloseHandle +DuplicateHandle +GetFileSizeEx \ No newline at end of file diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/InternalLogger.cs b/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/InternalLogger.cs index b54b624..df61b4d 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/InternalLogger.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/InternalLogger.cs @@ -1,14 +1,22 @@ -using Microsoft.Extensions.Logging; +using System; +using Microsoft.Extensions.Logging; using NLog; using NLog.Extensions.Logging; namespace Lithnet.CredentialProvider.Samples { - internal static class InternalLogger + internal class InternalLoggerFactory : ICredentialProviderLoggerFactory { - internal static ILoggerFactory LoggerFactory { get; } + internal static ICredentialProviderLoggerFactory Instance { get; } - static InternalLogger() + private ILoggerFactory loggerFactory; + + public InternalLoggerFactory(ILoggerFactory factory) + { + this.loggerFactory = factory; + } + + static InternalLoggerFactory() { /* This sample uses NLog to capture trace events from the provider, but you can use any @@ -30,7 +38,53 @@ namespace Lithnet.CredentialProvider.Samples config.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, logconsole); LogManager.Configuration = config; - InternalLogger.LoggerFactory = new NLogLoggerFactory(new NLogLoggerProvider(new NLogProviderOptions() { ReplaceLoggerFactory = true }, LogManager.LogFactory)); + var loggerFactory = new NLogLoggerFactory(new NLogLoggerProvider(new NLogProviderOptions() { ReplaceLoggerFactory = true }, LogManager.LogFactory)); + InternalLoggerFactory.Instance = new InternalLoggerFactory(loggerFactory); + } + + public ICredentialProviderLogger CreateLogger(Type type) + { + return new CredentialProviderLogger(loggerFactory.CreateLogger(type)); + } + + public ICredentialProviderLogger CreateLogger() + { + return new CredentialProviderLogger(loggerFactory.CreateLogger()); + } + } + + public class CredentialProviderLogger : ICredentialProviderLogger + { + private readonly Microsoft.Extensions.Logging.ILogger logger; + + public CredentialProviderLogger(Microsoft.Extensions.Logging.ILogger logger) + { + this.logger = logger; + } + + public void LogError(Exception ex, string message) + { + this.logger.LogError(ex, message); + } + + public void LogError(string message) + { + this.logger.LogError(message); + } + + public void LogTrace(string message) + { + this.logger.LogTrace(message); + } + + public void LogInformation(string message) + { + this.logger.LogInformation(message); + } + + public void LogWarning(string message) + { + this.logger.LogWarning(message); } } } diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderNet472x64.cs b/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderNet472x64.cs index 8486d7f..8346cfb 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderNet472x64.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderNet472x64.cs @@ -14,9 +14,9 @@ namespace Lithnet.CredentialProvider.Samples [Guid("4eb911fa-ca18-40ea-86df-19aff5d1da58")] public class TestCredentialProviderNet472x64 : CredentialProviderBase { - protected override ILoggerFactory GetLoggerFactory() + protected override ICredentialProviderLoggerFactory GetLoggerFactory() { - return InternalLogger.LoggerFactory; + return InternalLoggerFactory.Instance; } public override IEnumerable GetControls(UsageScenario cpus) diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderTile.cs b/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderTile.cs index 0720de8..adb26e8 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderTile.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.net472.x64/TestCredentialProviderTile.cs @@ -15,7 +15,7 @@ namespace Lithnet.CredentialProvider.Samples private SmallLabelControl CheckboxStateControl; private SmallLabelControl ComboboxStateControl; - private ILogger logger = InternalLogger.LoggerFactory.CreateLogger(); + private ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger(); public TestCredentialProviderTile(CredentialProviderBase credentialProvider) : base(credentialProvider) { diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net472.x86/TestCredentialProviderNet472x86.cs b/src/samples/Lithnet.CredentialProvider.Sample.net472.x86/TestCredentialProviderNet472x86.cs index c4ce35c..9bf6ad8 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net472.x86/TestCredentialProviderNet472x86.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.net472.x86/TestCredentialProviderNet472x86.cs @@ -13,11 +13,11 @@ namespace Lithnet.CredentialProvider.Samples [Guid("c9055c88-03f9-4a12-8e33-1ee75826a4a6")] public class TestCredentialProviderNet472x86 : CredentialProviderBase { - private static readonly ILogger logger = InternalLogger.LoggerFactory.CreateLogger(); + private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger(); - protected override ILoggerFactory GetLoggerFactory() + protected override ICredentialProviderLoggerFactory GetLoggerFactory() { - return InternalLogger.LoggerFactory; + return InternalLoggerFactory.Instance; } public override IEnumerable GetControls(UsageScenario cpus) diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/TestCredentialProviderNet60x64.cs b/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/TestCredentialProviderNet60x64.cs index 774b7b5..1ef2d92 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/TestCredentialProviderNet60x64.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x64/TestCredentialProviderNet60x64.cs @@ -13,11 +13,11 @@ namespace Lithnet.CredentialProvider.Samples [Guid("4cd12d80-9259-4f38-94dc-1828080ad9ff")] public class TestCredentialProviderNet60x64 : CredentialProviderBase { - private static readonly ILogger logger = InternalLogger.LoggerFactory.CreateLogger(); + private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger(); - protected override ILoggerFactory GetLoggerFactory() + protected override ICredentialProviderLoggerFactory GetLoggerFactory() { - return InternalLogger.LoggerFactory; + return InternalLoggerFactory.Instance; } public override IEnumerable GetControls(UsageScenario cpus) diff --git a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/TestCredentialProviderNet60x86.cs b/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/TestCredentialProviderNet60x86.cs index 035cec6..8b4d847 100644 --- a/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/TestCredentialProviderNet60x86.cs +++ b/src/samples/Lithnet.CredentialProvider.Sample.net6.0.x86/TestCredentialProviderNet60x86.cs @@ -13,11 +13,11 @@ namespace Lithnet.CredentialProvider.Samples [Guid("90592593-f4d3-4f62-aa83-9cf1f7b590e0")] public class TestCredentialProviderNet60x86 : CredentialProviderBase { - private static readonly ILogger logger = InternalLogger.LoggerFactory.CreateLogger(); + private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger(); - protected override ILoggerFactory GetLoggerFactory() + protected override ICredentialProviderLoggerFactory GetLoggerFactory() { - return InternalLogger.LoggerFactory; + return InternalLoggerFactory.Instance; } public override IEnumerable GetControls(UsageScenario cpus)