Adds initial support for reading parameters passed to consent UI
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="ConsentUIData.ConsentUIType"/> property to determine the type of data structure and cast it to one of the concrete implementations.
|
||||
/// </summary>
|
||||
public abstract class ConsentUIData
|
||||
{
|
||||
private static bool? isConsentUI;
|
||||
private static ConsentUICommandLineArgs commandLineArgs;
|
||||
private protected ConsentUIStructureHeader header;
|
||||
private readonly byte[] rawData;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating the type of ConsentUI data structure
|
||||
/// </summary>
|
||||
public ConsentUIType Type => this.header.Type;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating the consent prompt type
|
||||
/// </summary>
|
||||
public int PromptType => this.header.PromptType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a handle to the Window that was responsible for invoking the ConsentUI prompt
|
||||
/// </summary>
|
||||
public IntPtr HWnd => this.header.hWnd;
|
||||
|
||||
public ElevationType ElevationType => this.header.elevationType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the session where the ConsentUI prompt was originally invoked
|
||||
/// </summary>
|
||||
public int SessionId => this.header.sessionId;
|
||||
|
||||
private protected ConsentUIData(IntPtr pData, int expectedSize)
|
||||
{
|
||||
this.rawData = GetRawBytes(pData, expectedSize);
|
||||
this.header = Marshal.PtrToStructure<ConsentUIStructureHeader>(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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Windows Identity from the original caller requesting elevation
|
||||
/// </summary>
|
||||
/// <returns>A WindowsIdentity object that represents the user requesting elevation</returns>
|
||||
/// <exception cref="Win32Exception">Thrown when the user's token could not be obtained from the session information</exception>
|
||||
public WindowsIdentity GetWindowsIdentity()
|
||||
{
|
||||
var duplicatedToken = DuplicateHandleInternal(this.header.hToken);
|
||||
return new WindowsIdentity(duplicatedToken.DangerousGetHandle());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a raw byte array representing the ConsentUI data structure
|
||||
/// </summary>
|
||||
/// <returns>A byte array</returns>
|
||||
public byte[] GetRawData()
|
||||
{
|
||||
return this.rawData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string value that is packed at the end of the data structure if the offset is valid
|
||||
/// </summary>
|
||||
/// <param name="pData">A pointer to the start of the data structure</param>
|
||||
/// <param name="offset">The position from the start of the data structure where the string starts</param>
|
||||
/// <returns>A string containing all characters from the given offset up to the first null character found</returns>
|
||||
private protected string GetStringValueIfValid(IntPtr pData, int offset)
|
||||
{
|
||||
if (offset > 0)
|
||||
{
|
||||
this.ThrowOnInvalidOffset(offset);
|
||||
return Marshal.PtrToStringUni(IntPtr.Add(pData, offset));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an exception if the given offset is greater than the size of the data structure
|
||||
/// </summary>
|
||||
/// <param name="value">The value of the offset</param>
|
||||
/// <exception cref="InvalidDataException">Thrown when the value of the pointer is greater than the expected data size</exception>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ConsentUIData object from a previously-obtained raw byte representation
|
||||
/// </summary>
|
||||
/// <param name="consentUIDataStructure">The raw bytes of a supported ConsentUI data structure</param>
|
||||
/// <returns>A ConsentUIData object</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data structure passed to the Consent UI process
|
||||
/// </summary>
|
||||
/// <returns>A ConsentUIData object</returns>
|
||||
public static ConsentUIData GetConsentUIData()
|
||||
{
|
||||
var pData = GetConsentUIData(out int structSize);
|
||||
return ConsentUIData.CreateInstance(pData.ToIntPtr(), structSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the current process is consent.exe, indicating that the provider is running inside an elevated UAC prompt
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw bytes of the ConsentUI data structure
|
||||
/// </summary>
|
||||
/// <returns>A byte array</returns>
|
||||
public static byte[] GetConsentUIDataRawBytes()
|
||||
{
|
||||
var pData = GetConsentUIData(out int structSize);
|
||||
return GetRawBytes(pData.ToIntPtr(), structSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the command line of the consent.exe process to retrieve the data structure passed to it
|
||||
/// </summary>
|
||||
/// <param name="size">Returns the size of the data structure as reported in the command line arguments</param>
|
||||
/// <returns>A pointer to the newly created copy of the data structure</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when either consent.exe is not the parent process</exception>
|
||||
/// <exception cref="ArgumentException">Throw when the arguments passed to consent.exe are invalid</exception>
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the command line arguments passed to the consent.exe process
|
||||
/// </summary>
|
||||
/// <returns>A ConsentUICommandLineArgs object containing the arguments parsed from the command line</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the arguments passed to consent.exe cannot be parsed or are of the incorrect number</exception>
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the memory from a raw pointer into a managed byte array
|
||||
/// </summary>
|
||||
/// <param name="pData">The pointer where the data copy must start</param>
|
||||
/// <param name="size">The number of bytes to copy</param>
|
||||
/// <returns>A copy of the raw memory returned as a managed byte array</returns>
|
||||
private static byte[] GetRawBytes(IntPtr pData, int size)
|
||||
{
|
||||
byte[] dataForExport = new byte[size];
|
||||
Marshal.Copy(pData, dataForExport, 0, size);
|
||||
return dataForExport;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the memory from a specified process
|
||||
/// </summary>
|
||||
/// <param name="processId">The ID of the process</param>
|
||||
/// <param name="address">The memory address to read</param>
|
||||
/// <param name="size">The size of the data at the specified memory address</param>
|
||||
/// <returns>A handle to a copy of the process memory</returns>
|
||||
/// <exception cref="Win32Exception">Thrown when the process could not be opened or the memory address could not be read</exception>
|
||||
/// <exception cref="InvalidDataException">Thrown when the size of the copied structure did not equal the expected size as passed to the method</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a native handle to a process
|
||||
/// </summary>
|
||||
/// <param name="processId">The ID of the process</param>
|
||||
/// <param name="rights">The requested access rights</param>
|
||||
/// <returns>A safe handle to the process</returns>
|
||||
/// <exception cref="Win32Exception">Thrown when the process handle could not be obtained</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the appropriate subclass of ConsentUIData by reading the type from the data structure
|
||||
/// </summary>
|
||||
/// <param name="pData">A pointer to the data structure</param>
|
||||
/// <param name="expectedSize">The expected size of the data structure</param>
|
||||
/// <returns>A ConsentUIData object</returns>
|
||||
/// <exception cref="InvalidDataException">Thrown when the size of the expected data structure does not match the size reported in the structure itself</exception>
|
||||
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"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Duplicates a handle passed in from the AppInfo service
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle to duplicate</param>
|
||||
/// <returns>A duplicated reference to the handle</returns>
|
||||
/// <exception cref="Win32Exception">Thrown when the handle could not be duplicated</exception>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace Lithnet.CredentialProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the data structure passed to consent UI when a user is trying to install an ActiveX control
|
||||
/// </summary>
|
||||
/// <remarks>This data structure is currently unknown and only the common header values are present</remarks>
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Lithnet.CredentialProvider.Interop;
|
||||
|
||||
namespace Lithnet.CredentialProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class ConsentUIDataCom : ConsentUIData
|
||||
{
|
||||
/// <summary>
|
||||
/// The path to the COM component that is requesting elevation
|
||||
/// </summary>
|
||||
public string ComComponentPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The resource path to the image to display in consent UI
|
||||
/// </summary>
|
||||
public string ImageResourcePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the process that is hosting the COM component
|
||||
/// </summary>
|
||||
public string ProcessPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A user friendly description of the type of operation that will be performed by the elevation
|
||||
/// </summary>
|
||||
public string OperationType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The CLSID of the COM component
|
||||
/// </summary>
|
||||
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<ConsentUIStructureCom>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace Lithnet.CredentialProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the data structure passed to consent UI in a yet unknown scenario
|
||||
/// </summary>
|
||||
/// <remarks>This data structure is currently unknown and only the common header values are present</remarks>
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Lithnet.CredentialProvider.Interop;
|
||||
|
||||
namespace Lithnet.CredentialProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the data structure passed to consent UI when a user is trying to elevate an executable
|
||||
/// </summary>
|
||||
public class ConsentUIDataExe : ConsentUIData
|
||||
{
|
||||
private IntPtr hFile;
|
||||
|
||||
/// <summary>
|
||||
/// The path to the process that the user has requested to be launched as an administrator
|
||||
/// </summary>
|
||||
public string ExecutablePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A currently unknown value. In most cases it seems to be the same as <see cref="ExecutablePath"/>
|
||||
/// </summary>
|
||||
public string Unknown1 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The full command line, including arguments that will be used to launch the executable
|
||||
/// </summary>
|
||||
public string CommandLine { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A currently unknown parameter
|
||||
/// </summary>
|
||||
public string Unknown2 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a handle to the executable that the user has requested to be launched as an administrator
|
||||
/// </summary>
|
||||
/// <returns>A handle to the executable</returns>
|
||||
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<ConsentUIStructureExe>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Lithnet.CredentialProvider.Interop;
|
||||
|
||||
namespace Lithnet.CredentialProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the data structure passed to consent UI when a user is trying to elevate an MSI installer
|
||||
/// </summary>
|
||||
public class ConsentUIDataMsi : ConsentUIData
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the product being installed
|
||||
/// </summary>
|
||||
public string ProductName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of the product being installed
|
||||
/// </summary>
|
||||
public string Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The locale of the product being installed
|
||||
/// </summary>
|
||||
public string Locale { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The publisher of the product being installed
|
||||
/// </summary>
|
||||
public string Publisher { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the MSI installer
|
||||
/// </summary>
|
||||
public string ExecutionPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the original MSI file launched by the user
|
||||
/// </summary>
|
||||
public string OriginalMsi { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A currently unknown parameter
|
||||
/// </summary>
|
||||
public string Unknown1 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A currently unknown parameter
|
||||
/// </summary>
|
||||
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<ConsentUIStructureMsi>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Lithnet.CredentialProvider.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the data structure passed to consent UI when a user is trying to elevate an MSIX package
|
||||
/// </summary>
|
||||
public class ConsentUIDataMsix : ConsentUIData
|
||||
{
|
||||
/// <summary>
|
||||
/// The path to the package being installed
|
||||
/// </summary>
|
||||
public string ExecutablePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the package being installed
|
||||
/// </summary>
|
||||
public string PackageName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The full command line, including any arguments used to launch the installer
|
||||
/// </summary>
|
||||
public string CommandLine { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A currently unknown parameter
|
||||
/// </summary>
|
||||
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<ConsentUIStructureMsix>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Lithnet.CredentialProvider
|
||||
{
|
||||
public enum ElevationType
|
||||
{
|
||||
Unknown1 = 0,
|
||||
Unknown2 = 1,
|
||||
Consent = 2,
|
||||
Credentials = 3
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -51,6 +51,16 @@ namespace Lithnet.CredentialProvider
|
||||
/// </summary>
|
||||
public IReadOnlyList<CredentialTile> Tiles { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates if this credential provider is loaded by Logon UI
|
||||
/// </summary>
|
||||
public bool IsLogonUI => this.UsageScenario == UsageScenario.Logon;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates if the credential provider is loaded by Consent UI (eg UAC prompt)
|
||||
/// </summary>
|
||||
public bool IsConsentUI => this.UsageScenario == UsageScenario.CredUI && ConsentUIData.IsConsentUIParent();
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to the serialized input data provided by CredUI
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Lithnet.CredentialProvider
|
||||
{
|
||||
public enum ConsentUIType
|
||||
{
|
||||
Exe = 0,
|
||||
Com = 1,
|
||||
Msi = 2,
|
||||
ActiveX = 3,
|
||||
CredCollect = 4,
|
||||
Msix = 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Lithnet.CredentialProvider.Interop
|
||||
{
|
||||
internal sealed class SafeHGlobalHandle : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Unmanaged pointer wrapped by this object
|
||||
/// </summary>
|
||||
IntPtr pointer;
|
||||
|
||||
SafeHGlobalHandle()
|
||||
{
|
||||
this.pointer = IntPtr.Zero;
|
||||
}
|
||||
|
||||
SafeHGlobalHandle(IntPtr handle)
|
||||
{
|
||||
this.pointer = handle;
|
||||
}
|
||||
|
||||
~SafeHGlobalHandle()
|
||||
{
|
||||
this.Dispose();
|
||||
}
|
||||
|
||||
public static SafeHGlobalHandle InvalidHandle => new SafeHGlobalHandle(IntPtr.Zero);
|
||||
|
||||
/// <summary>
|
||||
/// Operator to obtain the unmanaged pointer wrapped by the object. Note
|
||||
/// that the returned pointer is only valid for the lifetime of this
|
||||
/// object.
|
||||
/// </summary>
|
||||
/// <returns>Unmanaged pointer wrapped by the object</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@
|
||||
<Company>Lithnet</Company>
|
||||
<Copyright>Copyright 2023 Lithnet Pty Ltd</Copyright>
|
||||
<ProductName>Lithnet Windows Credential Provider</ProductName>
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<VersionSuffix>alpha1</VersionSuffix>
|
||||
<VersionPrefix>1.1.0</VersionPrefix>
|
||||
<VersionSuffix>beta1</VersionSuffix>
|
||||
<Authors>Lithnet</Authors>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<AutoIncrementPackageRevision>true</AutoIncrementPackageRevision>
|
||||
@@ -43,6 +43,9 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Drawing.Common" Version="6.0.0" Condition="$(TargetFrameworkIdentifier) != '.NETFramework'" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
ReadProcessMemory
|
||||
OpenProcess
|
||||
WTSQueryUserToken
|
||||
GetTokenInformation
|
||||
CloseHandle
|
||||
DuplicateHandle
|
||||
GetFileSizeEx
|
||||
@@ -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<T>()
|
||||
{
|
||||
return new CredentialProviderLogger(loggerFactory.CreateLogger<T>());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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<ControlBase> GetControls(UsageScenario cpus)
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ namespace Lithnet.CredentialProvider.Samples
|
||||
private SmallLabelControl CheckboxStateControl;
|
||||
private SmallLabelControl ComboboxStateControl;
|
||||
|
||||
private ILogger logger = InternalLogger.LoggerFactory.CreateLogger<TestCredentialProviderTile>();
|
||||
private ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger<TestCredentialProviderTile>();
|
||||
|
||||
public TestCredentialProviderTile(CredentialProviderBase credentialProvider) : base(credentialProvider)
|
||||
{
|
||||
|
||||
+3
-3
@@ -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<TestCredentialProviderNet472x86>();
|
||||
private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger<TestCredentialProviderNet472x86>();
|
||||
|
||||
protected override ILoggerFactory GetLoggerFactory()
|
||||
protected override ICredentialProviderLoggerFactory GetLoggerFactory()
|
||||
{
|
||||
return InternalLogger.LoggerFactory;
|
||||
return InternalLoggerFactory.Instance;
|
||||
}
|
||||
|
||||
public override IEnumerable<ControlBase> GetControls(UsageScenario cpus)
|
||||
|
||||
+3
-3
@@ -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<TestCredentialProviderNet60x64>();
|
||||
private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger<TestCredentialProviderNet60x64>();
|
||||
|
||||
protected override ILoggerFactory GetLoggerFactory()
|
||||
protected override ICredentialProviderLoggerFactory GetLoggerFactory()
|
||||
{
|
||||
return InternalLogger.LoggerFactory;
|
||||
return InternalLoggerFactory.Instance;
|
||||
}
|
||||
|
||||
public override IEnumerable<ControlBase> GetControls(UsageScenario cpus)
|
||||
|
||||
+3
-3
@@ -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<TestCredentialProviderNet60x86>();
|
||||
private static readonly ICredentialProviderLogger logger = InternalLoggerFactory.Instance.CreateLogger<TestCredentialProviderNet60x86>();
|
||||
|
||||
protected override ILoggerFactory GetLoggerFactory()
|
||||
protected override ICredentialProviderLoggerFactory GetLoggerFactory()
|
||||
{
|
||||
return InternalLogger.LoggerFactory;
|
||||
return InternalLoggerFactory.Instance;
|
||||
}
|
||||
|
||||
public override IEnumerable<ControlBase> GetControls(UsageScenario cpus)
|
||||
|
||||
Reference in New Issue
Block a user