├── .gitignore ├── assets └── example.png ├── PowerRemoteDesktop_LogonUI ├── App.config ├── Properties │ └── AssemblyInfo.cs ├── Exceptions.cs ├── Program.cs ├── PowerRemoteDesktop_LogonUI.csproj ├── DesktopThread.cs ├── WinLogon.cs └── Helper.cs ├── PowerRemoteDesktop_LogonUI.sln ├── README.md ├── LogonUI_PluginTester.ps1 └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | LogonUI_Snapshots/ 3 | PowerRemoteDesktop_LogonUI/bin/ 4 | PowerRemoteDesktop_LogonUI/obj/ -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkCoderSc/PowerRemoteDesktop_LogonUI/HEAD/assets/example.png -------------------------------------------------------------------------------- /PowerRemoteDesktop_LogonUI/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /PowerRemoteDesktop_LogonUI.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.32106.194 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerRemoteDesktop_LogonUI", "PowerRemoteDesktop_LogonUI\PowerRemoteDesktop_LogonUI.csproj", "{70468FA7-59F8-47C9-A12B-477F1F0AE372}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {70468FA7-59F8-47C9-A12B-477F1F0AE372}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {70468FA7-59F8-47C9-A12B-477F1F0AE372}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {70468FA7-59F8-47C9-A12B-477F1F0AE372}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {70468FA7-59F8-47C9-A12B-477F1F0AE372}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {925BBF36-3083-4B14-AA41-B043E7789E77} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /PowerRemoteDesktop_LogonUI/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("PowerRemoteDesktop_LogonUI")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("PowerRemoteDesktop_LogonUI")] 13 | [assembly: AssemblyCopyright("Copyright © 2022")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("70468fa7-59f8-47c9-a12b-477f1f0ae372")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /PowerRemoteDesktop_LogonUI/Exceptions.cs: -------------------------------------------------------------------------------- 1 | /******************************************************************************************************* 2 | * Jean-Pierre LESUEUR (@DarkCoderSc) * 3 | * https://twitter.com/darkcodersc * 4 | * https://github.com/DarkCoderSc * 5 | * www.phrozen.io * 6 | * jplesueur@phrozen.io * 7 | * PHROZEN * 8 | * * 9 | * License: * 10 | * Apache License * 11 | * Version 2.0, January 2004 * 12 | * http://www.apache.org/licenses/ * 13 | *******************************************************************************************************/ 14 | 15 | using System; 16 | using System.Runtime.InteropServices; 17 | 18 | namespace PowerRemoteDesktop_LogonUI 19 | { 20 | [Serializable] 21 | class WinApiException : Exception 22 | { 23 | public WinApiException(string winApiName): base(String.Format("WinAPI Error -> \"{0}\" with last error: {1}", winApiName, Marshal.GetLastWin32Error().ToString())) 24 | { 25 | 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /PowerRemoteDesktop_LogonUI/Program.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Jean-Pierre LESUEUR (@DarkCoderSc) 3 | * https://twitter.com/darkcodersc 4 | * https://github.com/DarkCoderSc 5 | * www.phrozen.io 6 | * jplesueur@phrozen.io 7 | * PHROZEN 8 | * 9 | * License: 10 | * Apache License 11 | * Version 2.0, January 2004 12 | * http://www.apache.org/licenses/ 13 | * 14 | * Description: 15 | * (Work In Progress) Plugin to capture WinLogon Desktop and control WinLogon Mouse / Keyboard. 16 | * This Plugin is expected to be used by PowerRemoteDesktop. 17 | * 18 | * https://github.com/DarkCoderSc/PowerRemoteDesktop 19 | */ 20 | 21 | using System; 22 | using System.Threading; 23 | using System.Runtime.InteropServices; 24 | 25 | namespace PowerRemoteDesktop_LogonUI 26 | { 27 | class Program 28 | { 29 | [DllImport("user32.dll", SetLastError = true)] 30 | public static extern bool SetProcessDPIAware(); 31 | 32 | static void Main(string[] args) 33 | { 34 | // Check if application is run as System user. 35 | string currentUserName = System.Security.Principal.WindowsIdentity.GetCurrent().Name; 36 | 37 | if (currentUserName != "NT AUTHORITY\\SYSTEM") 38 | { 39 | throw new Exception("This application must be run as user \"NT AUTHORITY\\SYSTEM\"."); 40 | } 41 | 42 | if (!Helper.CheckActiveTerminalSession()) 43 | { 44 | Helper.RespawnInActiveTerminalSession(); 45 | 46 | return; 47 | } 48 | 49 | /// 50 | SetProcessDPIAware(); 51 | /// 52 | 53 | // Protect from multiple instance 54 | using (Mutex mutex = new Mutex(false, "Global\\CDB9F615-962B-4B45-A9CD-1A7D7CA40920")) 55 | { 56 | if (mutex.WaitOne(0, false)) 57 | { 58 | // Create Workers 59 | Thread desktopThread = new Thread(DesktopThread.process); 60 | desktopThread.Start(); 61 | 62 | do 63 | { 64 | desktopThread.Join(1000); 65 | } while (Helper.CheckActiveTerminalSession()); 66 | 67 | if (desktopThread.IsAlive) 68 | { 69 | desktopThread.Abort(); 70 | } 71 | } 72 | } 73 | 74 | // If we are not in active terminal session, respawn in correct session 75 | if (!Helper.CheckActiveTerminalSession()) 76 | { 77 | Helper.RespawnInActiveTerminalSession(); 78 | 79 | return; 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerRemoteDesktop WinLogon (LogonUI) Plugin (Work In Progress) 2 | 3 | This Application is expected to be an extension of the PowerRemoteDesktop Server PowerShell Application. 4 | 5 | It allows to interact with active terminal session WinLogon (LogonUI) isolated desktop from default desktop. 6 | 7 | You can for example capture snapshot of: 8 | 9 | * UAC Prompt 10 | * Logon 11 | * CTRL+ALT+DEL Panel 12 | 13 | You can also interact with keyboard and mouse in this isolated desktop (WIP). 14 | 15 | ## Work In Progress 16 | 17 | I'm coding this plugin in parallel of PowerRemoteDesktop. This plugin is in its very early stage and is not yet even finished. 18 | 19 | Actually it only demonstrate that you can capture images of WinLogon isolated desktop from default desktop. 20 | 21 | I will continue to work on this plugin as soon as I finished the PowerRemoteDesktop v2.1 with Motion Update (My priority). 22 | 23 | ## How it Works 24 | 25 | As always, I'm really phobic of external dependencies / tools. 26 | 27 | The application must be run as user `NT AUTHORITY\SYSTEM`, to do so, through PowerShell, I'm creating a new `SYSTEM` task and run this task immediately (no need of psexec or custom Windows service). 28 | 29 | At this point, the issue is that Task Scheduler run the new task in **Terminal Session Id** `0` which doesn't have access to our active Terminal Session Desktops. 30 | 31 | To overcome this problem, when task is run in Session `0`, I'm scanning available Terminal Sessions for the **Active One** (Ex: Physical or RDP), when found I re-spawn current process in the correct Terminal Session. 32 | 33 | At this moment, we have our application running as `NT AUTHORITY\SYSTEM` in our current Terminal Session default Desktop. We can now legitimately switch from current default Desktop to WinLogon desktop (`WinSta0\winlogon`). 34 | 35 | We are now attached to **WinLogon** desktop and can interract with it (I/O). 36 | 37 | To detect when WinLogon desktop is spawned (UAC Prompt, Logon etc..) we simply need to check if a Winlogon Desktop Window is currently in Foreground. We don't need to monitor for Windows Message (I really like to simplify things). 38 | 39 | Now we need to transfer events from both default and winlogon desktop. To do so I'm simply using Named Pipe. 40 | 41 | You will find in current project a PowerShell Script that demonstrate how to: 42 | 43 | * Install and Run the Plugin. 44 | * Catch Winlogon Desktop Snapshots from default desktop and through Named Pipe. 45 | * Uninstall the Plugin. 46 | 47 | Thats it! 48 | 49 | ### How to test it 50 | 51 | First you need to compile the C# application using Visual Studio (In my case VS 2019) 52 | 53 | Then, you must open a new PowerShell Terminal as Administrator (Important) and `cd` to the root project directory. 54 | 55 | ⚠️ It is crutial to be located in project root directory since I'm using relative paths to simplify the testing process. 56 | 57 | Example: `cd Desktop\PowerRemoteDesktop_LogonUI` 58 | 59 | You can now launch the script using: 60 | 61 | (PS `PowerRemoteDesktop_LogonUI`) > `IEX(Get-Content .\LogonUI_PluginTester.ps1 -RAW)` 62 | 63 | It should monitor for WinLogon Events... 64 | 65 | Open for example `regedit.exe`, wait few seconds then press `CTRL+ALT+DEL` and go to **Update Password** panel. 66 | 67 | Check in the new folder (in root directory) called `LogonUI_Snapshots` for WinLogon Desktop images. 68 | 69 | ![Example](assets/example.png) 70 | 71 | -------------------------------------------------------------------------------- /PowerRemoteDesktop_LogonUI/PowerRemoteDesktop_LogonUI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {70468FA7-59F8-47C9-A12B-477F1F0AE372} 8 | Exe 9 | PowerRemoteDesktop_LogonUI 10 | PowerRemoteDesktop_LogonUI 11 | v4.7.2 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | true 37 | bin\x64\Debug\ 38 | DEBUG;TRACE 39 | full 40 | x64 41 | 7.3 42 | prompt 43 | true 44 | 45 | 46 | bin\x64\Release\ 47 | TRACE 48 | true 49 | pdbonly 50 | x64 51 | 7.3 52 | prompt 53 | true 54 | 55 | 56 | true 57 | bin\x86\Debug\ 58 | DEBUG;TRACE 59 | full 60 | x86 61 | 7.3 62 | prompt 63 | true 64 | 65 | 66 | bin\x86\Release\ 67 | TRACE 68 | true 69 | pdbonly 70 | x86 71 | 7.3 72 | prompt 73 | true 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /PowerRemoteDesktop_LogonUI/DesktopThread.cs: -------------------------------------------------------------------------------- 1 | /******************************************************************************************************* 2 | * Jean-Pierre LESUEUR (@DarkCoderSc) * 3 | * https://twitter.com/darkcodersc * 4 | * https://github.com/DarkCoderSc * 5 | * www.phrozen.io * 6 | * jplesueur@phrozen.io * 7 | * PHROZEN * 8 | * * 9 | * License: * 10 | * Apache License * 11 | * Version 2.0, January 2004 * 12 | * http://www.apache.org/licenses/ * 13 | *******************************************************************************************************/ 14 | 15 | using System; 16 | using System.IO; 17 | using System.Drawing; 18 | using System.IO.Pipes; 19 | using System.Windows.Forms; 20 | using System.Drawing.Imaging; 21 | using System.Runtime.InteropServices; 22 | 23 | namespace PowerRemoteDesktop_LogonUI 24 | { 25 | class DesktopThread 26 | { 27 | 28 | [DllImport("user32.dll", SetLastError = true)] 29 | public static extern IntPtr GetForegroundWindow(); 30 | 31 | static Bitmap DesktopSnapshot() 32 | { 33 | Screen primaryScreen = Screen.PrimaryScreen; 34 | 35 | Bitmap bitmap = new Bitmap( 36 | primaryScreen.Bounds.Width, 37 | primaryScreen.Bounds.Height 38 | ); 39 | 40 | Point location = new Point( 41 | primaryScreen.Bounds.Location.X, 42 | primaryScreen.Bounds.Location.Y 43 | ); 44 | 45 | Graphics graphics = Graphics.FromImage(bitmap); 46 | 47 | graphics.CopyFromScreen(location, Point.Empty, bitmap.Size); 48 | 49 | return bitmap; 50 | } 51 | 52 | public static void process() 53 | { 54 | using (WinLogon winLogon = new WinLogon()) 55 | { 56 | using (NamedPipeServerStream pipeServer = new NamedPipeServerStream("PowerRemoteDesktop_LogonUI", PipeDirection.Out)) 57 | { 58 | StreamWriter writer = new StreamWriter(pipeServer); 59 | 60 | while (true) 61 | { 62 | 63 | try 64 | { 65 | pipeServer.WaitForConnection(); 66 | 67 | writer.AutoFlush = true; 68 | 69 | if (GetForegroundWindow() != IntPtr.Zero) 70 | { 71 | writer.WriteLine("STREAM"); 72 | /// 73 | 74 | Bitmap bitmap = DesktopSnapshot(); 75 | if (bitmap != null) 76 | { 77 | System.IO.MemoryStream memoryStream = new MemoryStream(); 78 | try 79 | { 80 | bitmap.Save(memoryStream, ImageFormat.Jpeg); 81 | 82 | if (memoryStream.Length > 0) 83 | { 84 | writer.WriteLine(Convert.ToBase64String(memoryStream.ToArray())); 85 | } 86 | } 87 | finally 88 | { 89 | bitmap.Dispose(); 90 | 91 | if (memoryStream != null) 92 | { 93 | memoryStream.Dispose(); 94 | } 95 | } 96 | } 97 | } 98 | else 99 | { 100 | writer.WriteLine("NULL"); 101 | } 102 | 103 | if (pipeServer.IsConnected) 104 | { 105 | pipeServer.Disconnect(); 106 | } 107 | } 108 | catch (Exception ex) 109 | { 110 | break; 111 | } 112 | } 113 | } 114 | } 115 | 116 | return; 117 | } 118 | 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /PowerRemoteDesktop_LogonUI/WinLogon.cs: -------------------------------------------------------------------------------- 1 | /******************************************************************************************************* 2 | * Jean-Pierre LESUEUR (@DarkCoderSc) * 3 | * https://twitter.com/darkcodersc * 4 | * https://github.com/DarkCoderSc * 5 | * www.phrozen.io * 6 | * jplesueur@phrozen.io * 7 | * PHROZEN * 8 | * * 9 | * License: * 10 | * Apache License * 11 | * Version 2.0, January 2004 * 12 | * http://www.apache.org/licenses/ * 13 | *******************************************************************************************************/ 14 | 15 | using System; 16 | using System.Runtime.InteropServices; 17 | 18 | namespace PowerRemoteDesktop_LogonUI 19 | { 20 | 21 | class WinLogon: IDisposable 22 | { 23 | [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] 24 | public static extern IntPtr OpenWindowStation( 25 | [MarshalAs(UnmanagedType.LPTStr)] string WinStationName, 26 | [MarshalAs(UnmanagedType.Bool)] bool Inherit, 27 | uint Access 28 | ); 29 | 30 | [DllImport("user32.dll")] 31 | public static extern bool CloseWindowStation( 32 | IntPtr winStation 33 | ); 34 | 35 | [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] 36 | public static extern IntPtr OpenDesktop( 37 | [MarshalAs(UnmanagedType.LPTStr)] string DesktopName, 38 | uint Flags, 39 | bool Inherit, 40 | uint Access 41 | ); 42 | 43 | [DllImport("user32.dll", SetLastError = true)] 44 | public static extern bool CloseDesktop( 45 | IntPtr hDesktop 46 | ); 47 | 48 | [DllImport("user32.dll", SetLastError = true)] 49 | public static extern IntPtr GetProcessWindowStation(); 50 | 51 | [DllImport("user32.dll", SetLastError = true)] 52 | public static extern bool SetProcessWindowStation( 53 | IntPtr hWinSta 54 | ); 55 | 56 | [DllImport("user32.dll", SetLastError = true)] 57 | public static extern IntPtr GetThreadDesktop( 58 | uint dwThreadId 59 | ); 60 | 61 | [DllImport("user32.dll", SetLastError = true)] 62 | public static extern bool SetThreadDesktop( 63 | IntPtr hDesktop 64 | ); 65 | 66 | [DllImport("kernel32.dll", SetLastError = true)] 67 | public static extern uint GetCurrentThreadId(); 68 | 69 | /// 70 | 71 | private IntPtr _oldThreadDesktop = IntPtr.Zero; 72 | private IntPtr _oldProcessWinStation = IntPtr.Zero; 73 | private IntPtr _winSta0Station = IntPtr.Zero; 74 | private IntPtr _winLogonDesktop = IntPtr.Zero; 75 | 76 | private bool disposed = false; 77 | 78 | private void SwitchToWinLogonDesktop() 79 | { 80 | this._winSta0Station = OpenWindowStation("WinSta0", false, 0x2000000); 81 | if (this._winSta0Station == IntPtr.Zero) 82 | { 83 | throw new WinApiException("OpenWindowStation"); 84 | } 85 | 86 | this._oldProcessWinStation = GetProcessWindowStation(); 87 | 88 | if (!SetProcessWindowStation(this._winSta0Station)) 89 | { 90 | throw new WinApiException("SetProcessWindowStation"); 91 | } 92 | 93 | this._winLogonDesktop = OpenDesktop("winlogon", 0, false, 0x2000000); 94 | if (this._winLogonDesktop == IntPtr.Zero) 95 | { 96 | throw new WinApiException("OpenDesktop"); 97 | } 98 | 99 | this._oldThreadDesktop = GetThreadDesktop(GetCurrentThreadId()); 100 | 101 | if (!SetThreadDesktop(this._winLogonDesktop)) 102 | { 103 | throw new WinApiException("SetThreadDesktop"); 104 | } 105 | } 106 | 107 | private void RestoreOriginalDesktop() 108 | { 109 | if (this._oldThreadDesktop != IntPtr.Zero) 110 | { 111 | SetThreadDesktop(this._oldThreadDesktop); 112 | } 113 | 114 | if (this._winLogonDesktop != IntPtr.Zero) 115 | { 116 | CloseDesktop(this._winLogonDesktop); 117 | } 118 | 119 | if (this._oldProcessWinStation != IntPtr.Zero) 120 | { 121 | SetProcessWindowStation(this._oldProcessWinStation); 122 | } 123 | 124 | if (this._winSta0Station != IntPtr.Zero) 125 | { 126 | CloseWindowStation(this._winSta0Station); 127 | } 128 | } 129 | 130 | public bool AttachedToWinLogonDesktop() 131 | { 132 | return this._winLogonDesktop != IntPtr.Zero; 133 | } 134 | 135 | public void Dispose() 136 | { 137 | Dispose(disposing: true); 138 | 139 | GC.SuppressFinalize(this); 140 | } 141 | 142 | protected virtual void Dispose(bool disposing) 143 | { 144 | if (!this.disposed) 145 | { 146 | if (disposing) 147 | { 148 | this.RestoreOriginalDesktop(); 149 | } 150 | 151 | this.disposed = true; 152 | } 153 | } 154 | 155 | public WinLogon() 156 | { 157 | this.SwitchToWinLogonDesktop(); 158 | } 159 | 160 | ~WinLogon() 161 | { 162 | Dispose(disposing: false); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /LogonUI_PluginTester.ps1: -------------------------------------------------------------------------------- 1 | <#------------------------------------------------------------------------------- 2 | Power Remote Desktop LogonUI Tester 3 | 4 | .Developer 5 | Jean-Pierre LESUEUR (@DarkCoderSc) 6 | https://www.twitter.com/darkcodersc 7 | https://github.com/DarkCoderSc 8 | www.phrozen.io 9 | jplesueur@phrozen.io 10 | PHROZEN 11 | .License 12 | Apache License 13 | Version 2.0, January 2004 14 | http://www.apache.org/licenses/ 15 | 16 | .Description 17 | This is not a script to use in a production environment. 18 | This is ment to be used during the development of PowerRemoteDesktop_LogonUI C# Plugin. 19 | -------------------------------------------------------------------------------#> 20 | 21 | Add-Type -Assembly System.Windows.Forms 22 | Add-Type -Assembly System.Drawing 23 | Add-Type -MemberDefinition '[DllImport("User32.dll")] public static extern bool SetProcessDPIAware();' -Name User32 -Namespace W 24 | 25 | $global:LogonUITaskName = "PowerRemoteDesktop_LogonUI" 26 | $global:LogonUISnapshotDirectory = ".\LogonUI_Snapshots\" 27 | 28 | $null = [W.User32]::SetProcessDPIAware() 29 | 30 | function Install-WinLogonPlugin 31 | { 32 | <# 33 | .SYNOPSIS 34 | Create a new Microsoft Windows Task as NT AUTHORITY\SYSTEM. The LogonUI Plugin must be run as System User. 35 | This trick avoid the need of external tools like PSExec to run an application as System. 36 | 37 | .PARAMETER PluginPath 38 | Type: FilePath 39 | Default: None 40 | Description: LogonUI Plugin File Path. 41 | 42 | #> 43 | param ( 44 | [Parameter(Mandatory=$True)] 45 | [System.IO.FileInfo] $PluginPath 46 | ) 47 | 48 | if (-not (Get-ScheduledTask | Where-Object {$_.TaskName -like $global:LogonUITaskName })) 49 | { 50 | $taskDescription = "Run Power Remote Desktop LogonUI Plugin" 51 | 52 | $action = New-ScheduledTaskAction -Execute $PluginPath 53 | 54 | $null = Register-ScheduledTask -Force -Action $action -TaskName $global:LogonUITaskName -Description $taskDescription -User "NT AUTHORITY\SYSTEM" 55 | } 56 | 57 | Stop-ScheduledTask -TaskName $global:LogonUITaskName 58 | Start-ScheduledTask $global:LogonUITaskName 59 | } 60 | 61 | function Uninstall-WinLogonPlugin 62 | { 63 | <# 64 | .SYNOPSIS 65 | Remove WinLogon Plugin Task from Microsoft Task Scheduler. 66 | #> 67 | Stop-Process -Force -Name "PowerRemoteDesktop_LogonUI" -ErrorAction 'silentlycontinue' 68 | 69 | Unregister-ScheduledTask -TaskName $global:LogonUITaskName -Confirm:$false 70 | } 71 | 72 | function Invoke-WinLogonSnapshot 73 | { 74 | <# 75 | .SYNOPSIS 76 | Attempt to connect to open a named pipe connection with an existing LogonUI Instance. 77 | If visible and available, attempt to catch WinLogon Desktop Snapshot. 78 | #> 79 | $logonUIData = New-Object -TypeName PSCustomObject -Property @{ 80 | Success = $false 81 | Image = $null 82 | } 83 | try 84 | { 85 | $pipeClient = New-Object System.IO.Pipes.NamedPipeClientStream(".", "PowerRemoteDesktop_LogonUI", [System.IO.Pipes.PipeDirection]::In) 86 | 87 | $pipeClient.Connect() 88 | 89 | $reader = New-Object System.IO.StreamReader($pipeClient) 90 | 91 | 92 | switch($reader.ReadLine()) 93 | { 94 | "STREAM" 95 | { 96 | $stream = New-Object System.IO.MemoryStream 97 | try 98 | { 99 | [byte[]] $buffer = [System.Convert]::FromBase64String(($reader.ReadLine())) 100 | 101 | $stream.Write($buffer, 0, $buffer.Length) 102 | 103 | $stream.position = 0 104 | 105 | $logonUIData.Success = $true 106 | $logonUIData.Image = $stream 107 | } 108 | catch 109 | { 110 | if ($stream) 111 | { 112 | $stream.Close() 113 | } 114 | } 115 | finally 116 | { } 117 | } 118 | 119 | "NULL" 120 | { 121 | $logonUIData.Success = $true 122 | } 123 | } 124 | } 125 | catch 126 | { 127 | if ($bitmap) 128 | { 129 | $bitmap.Dispose() 130 | } 131 | } 132 | finally 133 | { 134 | if ($reader) 135 | { 136 | $reader.Dispose() 137 | } 138 | 139 | if ($pipeClient) 140 | { 141 | $pipeClient.Dispose() 142 | } 143 | } 144 | 145 | return $logonUIData 146 | } 147 | 148 | function Test-Administrator 149 | { 150 | <# 151 | .SYNOPSIS 152 | Return true if current PowerShell is running with Administrator privilege, otherwise return false. 153 | #> 154 | $windowsPrincipal = New-Object Security.Principal.WindowsPrincipal( 155 | [Security.Principal.WindowsIdentity]::GetCurrent() 156 | ) 157 | 158 | return $windowsPrincipal.IsInRole( 159 | [Security.Principal.WindowsBuiltInRole]::Administrator 160 | ) 161 | } 162 | 163 | 164 | function Invoke-WinLogonPlugin 165 | { 166 | <# 167 | .SYNOPSIS 168 | Install LogonUI Plugin and wait for WinLogon Desktop Snapshot. 169 | 170 | .PARAMETER PluginPath 171 | Type: FilePath 172 | Default: None 173 | Description: LogonUI Plugin File Path. 174 | 175 | #> 176 | param ( 177 | [Parameter(Mandatory=$True)] 178 | [System.IO.FileInfo] $PluginPath 179 | ) 180 | #> 181 | if (-not (Test-Administrator)) 182 | { 183 | throw "You must run this function as administrator." 184 | } 185 | 186 | $PluginPath = (Resolve-Path -Path $PluginPath).Path 187 | 188 | Install-WinLogonPlugin -PluginPath $PluginPath 189 | try 190 | { 191 | while ($true) 192 | { 193 | Measure-Command { 194 | $logonUIData = Invoke-WinLogonSnapshot 195 | } 196 | 197 | if ($logonUIData.Success -eq $true -and $logonUIData.Image) 198 | { 199 | $fileName = (Get-Date -format 'yyyy-MM-dd_HH-mm-ss-ffff') 200 | 201 | $image = [System.Drawing.Image]::FromStream($logonUIData.Image) 202 | 203 | $image.Save("$((Resolve-Path -Path $global:LogonUISnapshotDirectory).Path)$fileName.png", [System.Drawing.Imaging.ImageFormat]::Png) 204 | 205 | $logonUIData.Image.Close() 206 | } 207 | 208 | Start-Sleep -Seconds 1 209 | } 210 | } 211 | finally 212 | { 213 | Uninstall-WinLogonPlugin 214 | } 215 | } 216 | 217 | # Entry Point. 218 | # Modify bellow options only if you are sure of what you are doing. 219 | # Everything is relative but the PowerShell instance working directory that will run this script must 220 | # be set to project root directory. 221 | 222 | $ErrorActionPreference = "stop" 223 | 224 | if ($null -eq (Get-ChildItem | Where-Object -FilterScript { $_.Name -eq "PowerRemoteDesktop_LogonUI.sln" })) 225 | { 226 | throw "Error: Your PowerShell Terminal Working Directory must be set to PowerRemoteDesktop_LogonUI project root directory." 227 | } 228 | 229 | $null = New-Item -Path $global:LogonUISnapshotDirectory -ItemType Directory -ErrorAction "SilentlyContinue" 230 | 231 | Get-ChildItem -Path $global:LogonUISnapshotDirectory -Include "*.png" -File -Recurse | ForEach-Object { $_.Delete() } 232 | 233 | Invoke-WinLogonPlugin -PluginPath ".\PowerRemoteDesktop_LogonUI\bin\Debug\PowerRemoteDesktop_LogonUI.exe" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PowerRemoteDesktop_LogonUI/Helper.cs: -------------------------------------------------------------------------------- 1 | /******************************************************************************************************* 2 | * Jean-Pierre LESUEUR (@DarkCoderSc) * 3 | * https://twitter.com/darkcodersc * 4 | * https://github.com/DarkCoderSc * 5 | * www.phrozen.io * 6 | * jplesueur@phrozen.io * 7 | * PHROZEN * 8 | * * 9 | * License: * 10 | * Apache License * 11 | * Version 2.0, January 2004 * 12 | * http://www.apache.org/licenses/ * 13 | *******************************************************************************************************/ 14 | 15 | using System; 16 | using System.Runtime.InteropServices; 17 | 18 | namespace PowerRemoteDesktop_LogonUI 19 | { 20 | class Helper 21 | { 22 | /* 23 | * 24 | * Windows API Constants 25 | * 26 | */ 27 | public const uint TOKEN_ALL_ACCESS = 0xF01FF; 28 | public const uint MAXIMUM_ALLOWED = 0x02000000; 29 | 30 | public const Int16 SW_SHOW = 0x5; 31 | public const Int16 STARTF_USESHOWWINDOW = 0x1; 32 | 33 | 34 | /* 35 | * 36 | * Windows API Enum / Structs 37 | * 38 | */ 39 | 40 | public enum WTS_CONNECTSTATE_CLASS 41 | { 42 | WTSActive, 43 | WTSConnected, 44 | WTSConnectQuery, 45 | WTSShadow, 46 | WTSDisconnected, 47 | WTSIdle, 48 | WTSListen, 49 | WTSReset, 50 | WTSDown, 51 | WTSInit 52 | } 53 | 54 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] 55 | private struct WTS_SESSION_INFO 56 | { 57 | public UInt32 SessionID; 58 | public string pWinStationName; 59 | public WTS_CONNECTSTATE_CLASS State; 60 | } 61 | 62 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 63 | struct STARTUPINFO 64 | { 65 | public Int32 cb; 66 | public string lpReserved; 67 | public string lpDesktop; 68 | public string lpTitle; 69 | public Int32 dwX; 70 | public Int32 dwY; 71 | public Int32 dwXSize; 72 | public Int32 dwYSize; 73 | public Int32 dwXCountChars; 74 | public Int32 dwYCountChars; 75 | public Int32 dwFillAttribute; 76 | public Int32 dwFlags; 77 | public Int16 wShowWindow; 78 | public Int16 cbReserved2; 79 | public IntPtr lpReserved2; 80 | public IntPtr hStdInput; 81 | public IntPtr hStdOutput; 82 | public IntPtr hStdError; 83 | } 84 | 85 | [StructLayout(LayoutKind.Sequential)] 86 | internal struct PROCESS_INFORMATION 87 | { 88 | public IntPtr hProcess; 89 | public IntPtr hThread; 90 | public int dwProcessId; 91 | public int dwThreadId; 92 | } 93 | 94 | public enum SECURITY_IMPERSONATION_LEVEL 95 | { 96 | SecurityAnonymous, 97 | SecurityIdentification, 98 | SecurityImpersonation, 99 | SecurityDelegation 100 | } 101 | 102 | public enum TOKEN_TYPE 103 | { 104 | TokenPrimary = 1, 105 | TokenImpersonation 106 | } 107 | 108 | public enum TOKEN_INFORMATION_CLASS 109 | { 110 | TokenUser = 1, 111 | TokenGroups, 112 | TokenPrivileges, 113 | TokenOwner, 114 | TokenPrimaryGroup, 115 | TokenDefaultDacl, 116 | TokenSource, 117 | TokenType, 118 | TokenImpersonationLevel, 119 | TokenStatistics, 120 | TokenRestrictedSids, 121 | TokenSessionId, 122 | TokenGroupsAndPrivileges, 123 | TokenSessionReference, 124 | TokenSandBoxInert, 125 | TokenAuditPolicy, 126 | TokenOrigin, 127 | TokenElevationType, 128 | TokenLinkedToken, 129 | TokenElevation, 130 | TokenHasRestrictions, 131 | TokenAccessInformation, 132 | TokenVirtualizationAllowed, 133 | TokenVirtualizationEnabled, 134 | TokenIntegrityLevel, 135 | TokenUIAccess, 136 | TokenMandatoryPolicy, 137 | TokenLogonSid, 138 | MaxTokenInfoClass 139 | } 140 | 141 | /* 142 | * 143 | * Windows API Defs 144 | * 145 | */ 146 | 147 | // WTSAPI32.DLL 148 | 149 | [DllImport("wtsapi32.dll", SetLastError = true)] 150 | static extern bool WTSQueryUserToken(UInt32 sessionId, out IntPtr Token); 151 | 152 | [DllImport("wtsapi32.dll", SetLastError = true)] 153 | static extern bool WTSEnumerateSessions( 154 | IntPtr hServer, 155 | UInt32 Reserved, 156 | UInt32 Version, 157 | out IntPtr ppSessionInfo, 158 | out UInt32 pCount 159 | ); 160 | 161 | [DllImport("wtsapi32.dll")] 162 | static extern void WTSFreeMemory(IntPtr pMemory); 163 | 164 | // ADVAPI32.DLL 165 | 166 | 167 | 168 | [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 169 | static extern bool CreateProcessAsUser( 170 | IntPtr hToken, 171 | string lpApplicationName, 172 | IntPtr lpCommandLine, 173 | IntPtr lpProcessAttributes, 174 | IntPtr lpThreadAttributes, 175 | bool bInheritHandles, 176 | uint dwCreationFlags, 177 | IntPtr lpEnvironment, 178 | IntPtr lpCurrentDirectory, 179 | ref STARTUPINFO lpStartupInfo, 180 | out PROCESS_INFORMATION lpProcessInformation 181 | ); 182 | 183 | [DllImport("advapi32.dll", SetLastError = true)] 184 | static extern bool OpenProcessToken( 185 | IntPtr ProcessHandle, 186 | UInt32 DesiredAccess, 187 | out IntPtr TokenHandle 188 | ); 189 | 190 | [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] 191 | public extern static bool DuplicateTokenEx( 192 | IntPtr hExistingToken, 193 | uint dwDesiredAccess, 194 | IntPtr lpTokenAttributes, 195 | SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, 196 | TOKEN_TYPE TokenType, 197 | out IntPtr phNewToken 198 | ); 199 | 200 | [DllImport("advapi32.dll", SetLastError = true)] 201 | public extern static bool SetTokenInformation( 202 | IntPtr TokenHandle, 203 | TOKEN_INFORMATION_CLASS TokenInformationClass, 204 | ref UInt32 TokenInformation, 205 | UInt32 TokenInformationLength 206 | ); 207 | 208 | // KERNEL32.DLL 209 | 210 | [DllImport("kernel32.dll", SetLastError = true)] 211 | public static extern IntPtr GetCurrentProcess(); 212 | 213 | [DllImport("kernel32.dll", SetLastError = true)] 214 | public static extern bool CloseHandle(IntPtr handle); 215 | 216 | [DllImport("kernel32.dll", SetLastError = true)] 217 | public static extern UInt32 GetCurrentProcessId(); 218 | 219 | [DllImport("Kernel32.dll", SetLastError = true)] 220 | public static extern bool ProcessIdToSessionId(UInt32 processID, out UInt32 sessionID); 221 | 222 | //--- 223 | 224 | /// 225 | /// Check whether or not current process is sitting in active terminal session. 226 | /// 227 | /// True if we are already sitting in active terminal session otherwise false. 228 | public static bool CheckActiveTerminalSession() 229 | { 230 | UInt32 sessionId = 0; 231 | 232 | ProcessIdToSessionId(GetCurrentProcessId(), out sessionId); 233 | 234 | return sessionId == GetActiveTerminalSessionId(); 235 | } 236 | 237 | /// 238 | /// Retrieve the active terminal session Id. 239 | /// 240 | public static UInt32 GetActiveTerminalSessionId() 241 | { 242 | IntPtr pSessionArray = IntPtr.Zero; 243 | UInt32 sessionCount = 0; 244 | try 245 | { 246 | if (!WTSEnumerateSessions(IntPtr.Zero, 0, 1, out pSessionArray, out sessionCount)) 247 | { 248 | throw new WinApiException("WTSEnumerateSessions"); 249 | } 250 | 251 | UInt32 activeSessionId = 0; 252 | bool sessionFound = false; 253 | 254 | // Iterate through each session 255 | for (var i = 0; i < sessionCount; i++) 256 | { 257 | IntPtr pOffset = (IntPtr)((uint)pSessionArray + (i * Marshal.SizeOf(typeof(WTS_SESSION_INFO)))); 258 | 259 | WTS_SESSION_INFO session = (WTS_SESSION_INFO)Marshal.PtrToStructure(pOffset, typeof(WTS_SESSION_INFO)); 260 | 261 | if (session.State == WTS_CONNECTSTATE_CLASS.WTSActive) 262 | { 263 | activeSessionId = session.SessionID; 264 | 265 | sessionFound = true; 266 | 267 | break; 268 | } 269 | } 270 | 271 | if (!sessionFound) 272 | { 273 | throw new Exception("Could not find active terminal session."); 274 | } 275 | 276 | return activeSessionId; 277 | } 278 | finally 279 | { 280 | if (pSessionArray != IntPtr.Zero) 281 | { 282 | WTSFreeMemory(pSessionArray); 283 | } 284 | } 285 | } 286 | 287 | /// 288 | /// Spawn a new instance of current application in the active terminal session id. 289 | /// 290 | public static void RespawnInActiveTerminalSession() 291 | { 292 | IntPtr token = IntPtr.Zero; 293 | IntPtr newToken = IntPtr.Zero; 294 | try 295 | { 296 | UInt32 sessionId = GetActiveTerminalSessionId(); 297 | 298 | if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, out token)) 299 | { 300 | throw new WinApiException("OpenProcessToken"); 301 | } 302 | 303 | if (!DuplicateTokenEx(token, MAXIMUM_ALLOWED, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, TOKEN_TYPE.TokenPrimary, out newToken)) 304 | { 305 | throw new WinApiException("DuplicateTokenEx"); 306 | } 307 | 308 | if (!SetTokenInformation(newToken, TOKEN_INFORMATION_CLASS.TokenSessionId, ref sessionId, (UInt32)Marshal.SizeOf(sessionId))) 309 | { 310 | throw new WinApiException("SetTokenInformation"); 311 | } 312 | 313 | STARTUPINFO startupInfo = new STARTUPINFO(); 314 | startupInfo.cb = Marshal.SizeOf(startupInfo); 315 | startupInfo.dwFlags = STARTF_USESHOWWINDOW; 316 | startupInfo.wShowWindow = SW_SHOW; 317 | 318 | PROCESS_INFORMATION processInformation; 319 | 320 | if (!CreateProcessAsUser( 321 | newToken, 322 | System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName, 323 | IntPtr.Zero, 324 | IntPtr.Zero, 325 | IntPtr.Zero, 326 | false, 327 | 0, 328 | IntPtr.Zero, 329 | IntPtr.Zero, 330 | ref startupInfo, 331 | out processInformation 332 | )) 333 | { 334 | throw new WinApiException("CreateProcessAsUser"); 335 | } 336 | } 337 | finally 338 | { 339 | if (token != IntPtr.Zero) 340 | { 341 | CloseHandle(token); 342 | } 343 | 344 | if (newToken != IntPtr.Zero) 345 | { 346 | CloseHandle(newToken); 347 | } 348 | } 349 | } 350 | } 351 | } 352 | --------------------------------------------------------------------------------