diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/Arcane_Server.psd1 b/Arcane_Server.psd1 new file mode 100644 index 0000000..948f7cd Binary files /dev/null and b/Arcane_Server.psd1 differ diff --git a/Arcane_Server.psm1 b/Arcane_Server.psm1 new file mode 100644 index 0000000..af38d09 --- /dev/null +++ b/Arcane_Server.psm1 @@ -0,0 +1,2795 @@ +<#------------------------------------------------------------------------------- + + Arcane :: Server + + .Developer + Jean-Pierre LESUEUR (@DarkCoderSc) + https://www.twitter.com/darkcodersc + https://www.github.com/PhrozenIO + https://github.com/DarkCoderSc + www.phrozen.io + jplesueur@phrozen.io + PHROZEN + + .License + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +-------------------------------------------------------------------------------#> + +Add-Type -Assembly System.Windows.Forms + +Add-Type @" + using System; + using System.Security; + using System.Runtime.InteropServices; + + public static class User32 + { + [DllImport("User32.dll")] + public static extern bool SetProcessDPIAware(); + + [DllImport("User32.dll")] + public static extern int LoadCursorA(int hInstance, int lpCursorName); + + [DllImport("User32.dll")] + public static extern bool GetCursorInfo(IntPtr pci); + + [DllImport("user32.dll")] + public static extern void mouse_event(int flags, int dx, int dy, int cButtons, int info); + + [DllImport("user32.dll")] + public static extern int GetSystemMetrics(int nIndex); + + [DllImport("User32.dll")] + public static extern IntPtr GetWindowDC(IntPtr hWnd); + + [DllImport("User32.dll")] + public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC); + + [DllImport("user32.dll")] + public static extern IntPtr GetDesktopWindow(); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern IntPtr OpenDesktop( + [MarshalAs(UnmanagedType.LPTStr)] string DesktopName, + uint Flags, + bool Inherit, + uint Access + ); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetThreadDesktop( + IntPtr hDesktop + ); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool CloseDesktop( + IntPtr hDesktop + ); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr GetForegroundWindow(); + } + + public static class Kernel32 + { + [DllImport("Kernel32.dll")] + public static extern uint SetThreadExecutionState(uint esFlags); + + [DllImport("kernel32.dll", SetLastError = true, EntryPoint="RtlMoveMemory"), SuppressUnmanagedCodeSecurity] + public static extern void CopyMemory( + IntPtr dest, + IntPtr src, + IntPtr count + ); + } + + public static class MSVCRT + { + [DllImport("msvcrt.dll", CallingConvention=CallingConvention.Cdecl), SuppressUnmanagedCodeSecurity] + public static extern IntPtr memcmp( + IntPtr p1, + IntPtr p2, + IntPtr count + ); + } + + public static class GDI32 + { + [DllImport("gdi32.dll")] + public static extern IntPtr DeleteDC(IntPtr hDc); + + [DllImport("gdi32.dll")] + public static extern IntPtr DeleteObject(IntPtr hDc); + + [DllImport("gdi32.dll"), SuppressUnmanagedCodeSecurity] + public static extern bool BitBlt( + IntPtr hdcDest, + int xDest, + int yDest, + int wDest, + int hDest, + IntPtr hdcSource, + int xSrc, + int ySrc, + int RasterOp + ); + + [DllImport("gdi32.dll")] + public static extern IntPtr CreateDIBSection( + IntPtr hdc, + IntPtr pbmi, + uint usage, + out IntPtr ppvBits, + IntPtr hSection, + uint offset + ); + + [DllImport ("gdi32.dll")] + public static extern IntPtr CreateCompatibleBitmap( + IntPtr hdc, + int nWidth, + int nHeight + ); + + [DllImport ("gdi32.dll")] + public static extern IntPtr CreateCompatibleDC(IntPtr hdc); + + [DllImport ("gdi32.dll")] + public static extern IntPtr SelectObject(IntPtr hdc, IntPtr bmp); + } +"@ + +$global:ArcaneVersion = "1.0.4" +$global:ArcaneProtocolVersion = "5.0.1" + +$global:HostSyncHash = [HashTable]::Synchronized(@{ + host = $host + ClipboardText = (Get-Clipboard -Raw) +}) + +enum ClipboardMode { + Disabled = 1 + Receive = 2 + Send = 3 + Both = 4 +} + +enum ProtocolCommand { + Success = 1 + Fail = 2 + RequestSession = 3 + AttachToSession = 4 + BadRequest = 5 + ResourceFound = 6 + ResourceNotFound = 7 + LogonUIAccessDenied = 8 + LogonUIWrongSession = 9 +} + +enum WorkerKind { + Desktop = 1 + Events = 2 +} + +enum LogKind { + Information + Warning + Success + Error +} + +enum BlockSize { + Size32 = 32 + Size64 = 64 + Size96 = 96 + Size128 = 128 + Size256 = 256 + Size512 = 512 +} + +enum PacketSize { + Size1024 = 1024 + Size2048 = 2048 + Size4096 = 4096 + Size8192 = 8192 + Size9216 = 9216 + Size12288 = 12288 + Size16384 = 16384 +} + +function Write-Banner +{ + <# + .SYNOPSIS + Output cool information about current PowerShell module to terminal. + #> + + Write-Host "" + Write-Host "Arcane Server " -NoNewLine + Write-Host $global:ArcaneVersion -ForegroundColor Cyan + Write-Host "Jean-Pierre LESUEUR (" -NoNewLine + Write-Host "@DarkCoderSc" -NoNewLine -ForegroundColor Green + Write-Host ") " -NoNewLine + Write-Host "" + Write-Host "License: Apache License (Version 2.0, January 2004)" + Write-Host "" +} + +function Write-Log +{ + <# + .SYNOPSIS + Output a log message to terminal with associated "icon". + + .PARAMETER Message + Type: String + Default: None + + Description: The message to write to terminal. + + .PARAMETER LogKind + Type: LogKind Enum + Default: Information + + Description: Define the logger "icon" kind. + #> + param( + [Parameter(Mandatory=$True)] + [string] $Message, + + [LogKind] $LogKind = [LogKind]::Information + ) + + switch ($LogKind) + { + ([LogKind]::Warning) + { + $icon = "!!" + $color = [System.ConsoleColor]::Yellow + + break + } + + ([LogKind]::Success) + { + $icon = "OK" + $color = [System.ConsoleColor]::Green + + break + } + + ([LogKind]::Error) + { + $icon = "KO" + $color = [System.ConsoleColor]::Red + + break + } + + default + { + $color = [System.ConsoleColor]::Cyan + $icon = "i" + } + } + + Write-Host "[ " -NoNewLine + Write-Host $icon -ForegroundColor $color -NoNewLine + Write-Host " ] $Message" +} + +function Write-OperationSuccessState +{ + param( + [Parameter(Mandatory=$True)] + $Result, + + [Parameter(Mandatory=$True)] + $Message + ) + + if ($Result) + { + $kind = [LogKind]::Success + } + else + { + $kind = [LogKind]::Error + } + + Write-Log -Message $Message -LogKind $kind +} + +function Invoke-PreventSleepMode +{ + <# + .SYNOPSIS + Prevent computer to enter sleep mode while server is running. + + .DESCRIPTION + Function returns thread execution state old flags value. You can use this old flags + to restore thread execution to its original state. + #> + + $ES_AWAYMODE_REQUIRED = [uint32]"0x00000040" + $ES_CONTINUOUS = [uint32]"0x80000000" + $ES_SYSTEM_REQUIRED = [uint32]"0x00000001" + + return [Kernel32]::SetThreadExecutionState( + $ES_CONTINUOUS -bor + $ES_SYSTEM_REQUIRED -bor + $ES_AWAYMODE_REQUIRED + ) +} + +function Update-ThreadExecutionState +{ + <# + .SYNOPSIS + Update current thread execution state flags. + + .PARAMETER Flags + Execution state flags. + #> + param( + [Parameter(Mandatory=$True)] + $Flags + ) + + return [Kernel32]::SetThreadExecutionState($Flags) -ne 0 +} + +function Get-PlainTextPassword +{ + <# + .SYNOPSIS + Retrieve the plain-text version of a secure string. + + .PARAMETER SecurePassword + The SecureString object to be reversed. + + #> + param( + [Parameter(Mandatory=$True)] + [SecureString] $SecurePassword + ) + + $BSTR = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword) + try + { + return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR) + } + finally + { + [Runtime.InteropServices.Marshal]::FreeBSTR($BSTR) + } +} + +function Test-PasswordComplexity +{ + <# + .SYNOPSIS + Check if password is sufficiently complex. + + .DESCRIPTION + To return True, Password must follow bellow complexity rules: + * Minimum 12 Characters. + * One of following symbols: "!@#%^&*_". + * At least of lower case character. + * At least of upper case character. + + .PARAMETER SecurePasswordCandidate + Type: SecureString + Default: None + Description: Secure String object containing the password to test. + #> + param ( + [Parameter(Mandatory=$True)] + [SecureString] $SecurePasswordCandidate + ) + + $complexityRules = "(?=^.{12,}$)(?=.*[!@#%^&*_]+)(?=.*[a-z])(?=.*[A-Z]).*$" + + return (Get-PlainTextPassword -SecurePassword $SecurePasswordCandidate) -match $complexityRules +} + +function New-RandomPassword +{ + <# + .SYNOPSIS + Generate a new secure password. + + .DESCRIPTION + Generate new password candidates until one candidate match complexity rules. + Generally only one iteration is enough but in some rare case it could be one or two more. + #> + do + { + $authorizedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#%^&*_" + + $candidate = -join ((1..18) | ForEach-Object { Get-Random -Input $authorizedChars.ToCharArray() }) + + $secureCandidate = ConvertTo-SecureString -String $candidate -AsPlainText -Force + } until (Test-PasswordComplexity -SecurePasswordCandidate $secureCandidate) + + $candidate = $null + + return $secureCandidate +} + +function Get-DefaultCertificateOrCreate +{ + <# + .SYNOPSIS + Get default certificate from user store or create a new one. + #> + param ( + [string] $SubjectName = "Arcane.Server", + [string] $StorePath = "cert:\CurrentUser\My", + [int] $CertExpirationInDays = 365 + ) + + $certificates = Get-ChildItem -Path $StorePath | Where-Object { $_.Subject -eq "CN=" + $SubjectName } + + if (-not $certificates) + { + return New-SelfSignedCertificate -CertStoreLocation $StorePath ` + -NotAfter (Get-Date).AddDays($CertExpirationInDays) ` + -Subject $SubjectName + } + else + { + return $certificates[0] + } +} + +function Get-SHA512FromString +{ + <# + .SYNOPSIS + Return the SHA512 value from string. + + .PARAMETER String + Type: String + Default : None + Description: A String to hash. + + .EXAMPLE + Get-SHA512FromString -String "Hello, World" + #> + param ( + [Parameter(Mandatory=$True)] + [string] $String + ) + + $buffer = [IO.MemoryStream]::new([byte[]][char[]]$String) + + return (Get-FileHash -InputStream $buffer -Algorithm SHA512).Hash +} + +function Resolve-AuthenticationChallenge +{ + <# + .SYNOPSIS + Algorithm to solve the server challenge during password authentication. + + .DESCRIPTION + Server needs to resolve the challenge and keep the solution in memory before sending + the candidate to remote peer. + + .PARAMETER Password + Type: SecureString + Default: None + Description: Secure String object containing the password for resolving challenge. + + .PARAMETER Candidate + Type: String + Default: None + Description: + Random string used to solve the challenge. This string is public and is set across network by server. + Each time a new connection is requested to server, a new candidate is generated. + + .EXAMPLE + Resolve-AuthenticationChallenge -Password "s3cr3t!" -Candidate "rKcjdh154@]=Ldc" + #> + param ( + [Parameter(Mandatory=$True)] + [SecureString] $SecurePassword, + + [Parameter(Mandatory=$True)] + [string] $Candidate + ) + + $pbkdf2 = New-Object System.Security.Cryptography.Rfc2898DeriveBytes( + (Get-PlainTextPassword -SecurePassword $SecurePassword), + [Text.Encoding]::UTF8.GetBytes($Candidate), + 1000, + [System.Security.Cryptography.HashAlgorithmName]::SHA512 + ) + try + { + return -join ($pbkdf2.GetBytes(64) | ForEach-Object { "{0:X2}" -f $_ }) + } + finally { + $pbkdf2.Dispose() + } +} + +$global:DesktopStreamScriptBlock = { + function Get-ScreenList() + { + <# + .SYNOPSIS + Return an array of screen objects. + + .DESCRIPTION + A screen refer to physical or virtual screen (monitor). + + #> + $result = @() + + $screens = ([System.Windows.Forms.Screen]::AllScreens | Sort-Object -Property Primary -Descending) + + $i = 0 + foreach ($screen in $screens) + { + $i++ + + # Take a snapshot of the screen (16:9 aspect ratio) + # $scaledWidth = 260 + # $scaledHeight = 146 + + <# + $bitmap = $null + $graphics = $null + $scaledBitmap = $null + $scaledGraphics = $null + $memoryStream = $null + + try + { + $bitmap = New-Object System.Drawing.Bitmap($screen.Bounds.Width, $screen.Bounds.Height) + + $graphics = [System.Drawing.Graphics]::FromImage($bitmap) + + $graphics.CopyFromScreen(0, 0, 0, 0, $bitmap.Size) + + $scaledBitmap = New-Object System.Drawing.Bitmap($scaledWidth, $scaledHeight) + + $scaledGraphics = [System.Drawing.Graphics]::FromImage($scaledBitmap) + + $scaledGraphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality + $scaledGraphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality + $scaledGraphics.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality + $scaledGraphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic + + $scaledGraphics.DrawImage($bitmap, 0, 0, $scaledWidth, $scaledHeight) + + $memoryStream = New-Object System.IO.MemoryStream + $scaledBitmap.Save($memoryStream, [System.Drawing.Imaging.ImageFormat]::Jpeg) + $memoryStream.Position = 0 + + $preview = [Convert]::ToBase64String($memoryStream.ToArray()) + } + finally + { + if ($bitmap) + { + $bitmap.Dispose() + } + + if ($graphics) + { + $graphics.Dispose() + } + + if ($scaledBitmap) + { + $scaledBitmap.Dispose() + } + + if ($scaledGraphics) + { + $scaledGraphics.Dispose() + } + + if ($memoryStream) + { + $memoryStream.Dispose() + } + } + #> + + # Append screen object to result array + $result += New-Object -TypeName PSCustomObject -Property @{ + Id = $i + Name = $screen.DeviceName + Primary = $screen.Primary + Width = $screen.Bounds.Width + Height = $screen.Bounds.Height + X = $screen.Bounds.X + Y = $screen.Bounds.Y + # Preview = $preview + } + } + + return ,$result + } + + $mirrorDesktop_DC = [IntPtr]::Zero + $desktop_DC = [IntPtr]::Zero + $mirrorDesktop_hBmp = [IntPtr]::Zero + $spaceBlock_DC = [IntPtr]::Zero + $spaceBlock_hBmp = [IntPtr]::Zero + $dirtyRect_DC = [IntPtr]::Zero + $pBitmapInfoHeader = [IntPtr]::Zero + + $SRCCOPY = 0x00CC0020 + $DIB_RGB_COLORS = 0x0 + try + { + $screens = New-Object PSCustomObject -Property @{ + List = (Get-ScreenList) + } + $Param.Client.WriteJson($screens) + + $screen = $null + + $viewerExpectation = $Param.Client.ReadLine() | ConvertFrom-Json + + if ($viewerExpectation.PSobject.Properties.name -contains "ScreenName") + { + $screen = [System.Windows.Forms.Screen]::AllScreens | Where-Object -FilterScript { + $_.DeviceName -eq $viewerExpectation.ScreenName + } + + # TODO: Add other parameters + } + + if ($screen -eq $null) + { + $screen = [System.Windows.Forms.Screen]::PrimaryScreen + } + + # Default + $blockSize = 64 + $packetSize = 4096 + $compressionQuality = 100 + + # User-defined (Optional) + if ($viewerExpectation.PSobject.Properties.name -contains "BlockSize") + { + $blockSize = $viewerExpectation.BlockSize + } + + if ($viewerExpectation.PSobject.Properties.name -contains "PacketSize") + { + $packetSize = $viewerExpectation.PacketSize + } + + if ($viewerExpectation.PSobject.Properties.name -contains "ImageCompressionQuality") + { + $compressionQuality = $viewerExpectation.ImageCompressionQuality + } + + $screenBounds = $screen.Bounds + + $SpaceGrid = $null + $horzBlockCount = [math]::ceiling($screenBounds.Width / $blockSize) + $vertBlockCount = [math]::ceiling($screenBounds.Height / $blockSize) + + $encoderParameters = New-Object System.Drawing.Imaging.EncoderParameters(1) + $encoderParameters.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter( + [System.Drawing.Imaging.Encoder]::Quality, + $compressionQuality + ) + + $encoder = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' }; + + $SpaceGrid = New-Object IntPtr[][] $vertBlockCount, $horzBlockCount + + $firstIteration = $true + + # Create our desktop mirror (For speeding up BitBlt calls) + + [IntPtr] $desktop_DC = [User32]::GetWindowDC([User32]::GetDesktopWindow()) + [IntPtr] $mirrorDesktop_DC = [GDI32]::CreateCompatibleDC($desktop_DC) + + [IntPtr] $mirrorDesktop_hBmp = [GDI32]::CreateCompatibleBitmap( + $desktop_DC, + $screenBounds.Width, + $screenBounds.Height + ) + + $null = [GDI32]::SelectObject($mirrorDesktop_DC, $mirrorDesktop_hBmp) + + # Create our block of space for change detection + + <# + typedef struct tagBITMAPINFOHEADER { + // x86-32|64: 0x4 Bytes | Padding = 0x0 | Offset: 0x0 + DWORD biSize; + + // x86-32|64: 0x4 Bytes | Padding = 0x0 | Offset: 0x4 + LONG biWidth; + + // x86-32|64: 0x4 Bytes | Padding = 0x0 | Offset: 0x8 + LONG biHeight; + + // x86-32|64: 0x2 Bytes | Padding = 0x0 | Offset: 0xc + WORD biPlanes; + + // x86-32|64: 0x2 Bytes | Padding = 0x0 | Offset: 0xe + WORD biBitCount; + + // x86-32|64: 0x4 Bytes | Padding = 0x0 | Offset: 0x10 + DWORD biCompression; + + // x86-32|64: 0x4 Bytes | Padding = 0x0 | Offset: 0x14 + DWORD biSizeImage; + + // x86-32|64: 0x4 Bytes | Padding = 0x0 | Offset: 0x18 + LONG biXPelsPerMeter; + + // x86-32|64: 0x4 Bytes | Padding = 0x0 | Offset: 0x1c + LONG biYPelsPerMeter; + + // x86-32|64: 0x4 Bytes | Padding = 0x0 | Offset: 0x20 + DWORD biClrUsed; + + // x86-32|64: 0x4 Bytes | Padding = 0x0 | Offset: 0x24 + DWORD biClrImportant; + } BITMAPINFOHEADER, *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER; + + // x86-32|64 Struct Size: 0x28 (40 Bytes) + // BITMAPINFO = BITMAPINFOHEADER (0x28) + RGBQUAD (0x4) = 0x2c + #> + + $bitmapInfoHeaderSize = 0x28 + $bitmapInfoSize = $bitmapInfoHeaderSize + 0x4 + + $pBitmapInfoHeader = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($bitmapInfoSize) + + # ZeroMemory + for ($i = 0; $i -lt $bitmapInfoSize; $i++) + { + [System.Runtime.InteropServices.Marshal]::WriteByte($pBitmapInfoHeader, $i, 0x0) + } + + [System.Runtime.InteropServices.Marshal]::WriteInt32($pBitmapInfoHeader, 0x0, $bitmapInfoHeaderSize) # biSize + [System.Runtime.InteropServices.Marshal]::WriteInt32($pBitmapInfoHeader, 0x4, $blockSize) # biWidth + [System.Runtime.InteropServices.Marshal]::WriteInt32($pBitmapInfoHeader, 0x8, $blockSize) # biHeight + [System.Runtime.InteropServices.Marshal]::WriteInt16($pBitmapInfoHeader, 0xc, 0x1) # biPlanes + [System.Runtime.InteropServices.Marshal]::WriteInt16($pBitmapInfoHeader, 0xe, 0x20) # biBitCount + + [IntPtr] $spaceBlock_DC = [GDI32]::CreateCompatibleDC(0) + [IntPtr] $spaceBlock_Ptr = [IntPtr]::Zero + + [IntPtr] $spaceBlock_hBmp = [GDI32]::CreateDIBSection( + $spaceBlock_DC, + $pBitmapInfoHeader, + $DIB_RGB_COLORS, + [ref] $spaceBlock_Ptr, + [IntPtr]::Zero, + 0 + ) + + $null = [GDI32]::SelectObject($spaceBlock_DC, $spaceBlock_hBmp) + + # Create our dirty rect DC + $dirtyRect_DC = [GDI32]::CreateCompatibleDC(0) + + # SizeOf(DWORD) * 3 (SizeOf(Desktop) + SizeOf(Left) + SizeOf(Top)) + $sizeOfUInt32 = [Runtime.InteropServices.Marshal]::SizeOf([System.Type][UInt32]) + $struct = New-Object -TypeName byte[] -ArgumentList ($sizeOfUInt32 * 3) + + $topLeftBlock = [System.Drawing.Point]::Empty + $bottomRightBlock = [System.Drawing.Point]::Empty + + $blockMemSize = ((($blockSize * 32) + 32) -band -bnot 32) / 8 + $blockMemSize *= $blockSize + $ptrBlockMemSize = [IntPtr]::New($blockMemSize) + + $dirtyRect = New-Object -TypeName System.Drawing.Rectangle -ArgumentList 0, 0, $screenBounds.Width, $screenBounds.Height + + <# + $fps = 0 + $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + #> + + while ($Param.SafeHash.SessionActive) + { + # Refresh our desktop mirror + $result = [GDI32]::BitBlt( + $mirrorDesktop_DC, + 0, + 0, + $screenBounds.Width, + $screenBounds.Height, + $desktop_DC, + $screenBounds.Location.X, + $screenBounds.Location.Y, + $SRCCOPY + ) + + if (-not $result) + { + continue + } + + $updated = $false + + for ($y = 0; $y -lt $vertBlockCount; $y++) + { + for ($x = 0; $x -lt $horzBlockCount; $x++) + { + $null = [GDI32]::BitBlt( + $spaceBlock_DC, + 0, + 0, + $blockSize, + $blockSize, + $mirrorDesktop_DC, + ($x * $blockSize), + ($y * $blockSize), + $SRCCOPY + ); + + if ($firstIteration) + { + # Big bang occurs, tangent univers is getting created, where is Donnie? + $SpaceGrid[$y][$x] = [Runtime.InteropServices.Marshal]::AllocHGlobal($blockMemSize) + + [Kernel32]::CopyMemory($SpaceGrid[$y][$x], $spaceBlock_Ptr, $ptrBlockMemSize) + } + else + { + if ([MSVCRT]::memcmp($spaceBlock_Ptr, $SpaceGrid[$y][$x], $ptrBlockMemSize) -ne [IntPtr]::Zero) + { + [Kernel32]::CopyMemory($SpaceGrid[$y][$x], $spaceBlock_Ptr, $ptrBlockMemSize) + + if (-not $updated) + { + # Initialize with the first dirty block coordinates + $topLeftBlock.X = $x + $topLeftBlock.Y = $y + + $bottomRightBlock = $topLeftBlock + + $updated = $true + } + else + { + if ($x -lt $topLeftBlock.X) + { + $topLeftBlock.X = $x + } + + if ($y -lt $topLeftBlock.Y) + { + $topLeftBlock.Y = $y + } + + if ($x -gt $bottomRightBlock.X) + { + $bottomRightBlock.X = $x + } + + if ($y -gt $bottomRightBlock.Y) + { + $bottomRightBlock.Y = $y + } + } + } + } + } + } + + if ($updated) + { + # Create new updated rectangle pointing to the dirty region (since last snapshot) + $dirtyRect.X = $topLeftBlock.X * $blockSize + $dirtyRect.Y = $topLeftBlock.Y * $blockSize + + $dirtyRect.Width = (($bottomRightBlock.X * $blockSize) + $blockSize) - $dirtyRect.Left + $dirtyRect.Height = (($bottomRightBlock.Y * $blockSize) + $blockSize) - $dirtyRect.Top + } + + if ($updated -or $firstIteration) + { + try + { + $dirtyRect_hBmp = [GDI32]::CreateCompatibleBitmap( + $mirrorDesktop_DC, + $dirtyRect.Width, + $dirtyRect.Height + ) + + $null = [GDI32]::SelectObject($dirtyRect_DC, $dirtyRect_hBmp) + + $null = [GDI32]::BitBlt( + $dirtyRect_DC, + 0, + 0, + $dirtyRect.Width, + $dirtyRect.Height, + $mirrorDesktop_DC, + $dirtyRect.X, + $dirtyRect.Y, + $SRCCOPY + ) + + # TODO: Find a faster alternative + [System.Drawing.Bitmap] $updatedDesktop = [System.Drawing.Image]::FromHBitmap($dirtyRect_hBmp) + + $desktopStream = New-Object System.IO.MemoryStream + + $updatedDesktop.Save($desktopStream, $encoder, $encoderParameters) + + $desktopStream.Position = 0 + + try + { + # One call please + [System.Runtime.InteropServices.Marshal]::WriteInt32($struct, 0x0, $desktopStream.Length) + [System.Runtime.InteropServices.Marshal]::WriteInt32($struct, 0x4, $dirtyRect.Left) + [System.Runtime.InteropServices.Marshal]::WriteInt32($struct, 0x8, $dirtyRect.Top) + + $Param.Client.SSLStream.Write($struct , 0, $struct.Length) + + $binaryReader = New-Object System.IO.BinaryReader($desktopStream) + do + { + $bufferSize = ($desktopStream.Length - $desktopStream.Position) + if ($bufferSize -gt $packetSize) + { + $bufferSize = $packetSize + } + + $Param.Client.SSLStream.Write($binaryReader.ReadBytes($bufferSize), 0, $bufferSize) + } until ($desktopStream.Position -eq $desktopStream.Length) + } + catch + { + break + } + } + finally + { + if ($dirtyRect_hBmp -ne [IntPtr]::Zero) + { + $null = [GDI32]::DeleteObject($dirtyRect_hBmp) + } + + if ($desktopStream) + { + $desktopStream.Dispose() + } + + if ($updatedDesktop) + { + $updatedDesktop.Dispose() + } + } + } + + if ($firstIteration) + { + $firstIteration = $false + } + + <# + $fps++ + if ($Stopwatch.ElapsedMilliseconds -ge 1000) + { + $HostSyncHash.host.ui.WriteLine($fps) + $fps = 0 + + $Stopwatch.Restart() + } + #> + } + } + catch {} + finally + { + # Free allocated resources + if ($mirrorDesktop_DC -ne [IntPtr]::Zero) + { + $null = [GDI32]::DeleteDC($mirrorDesktop_DC) + } + + if ($mirrorDesktop_hBmp -ne [IntPtr]::Zero) + { + $null = [GDI32]::DeleteObject($mirrorDesktop_hBmp) + } + + if ($spaceBlock_DC -ne [IntPtr]::Zero) + { + $null = [GDI32]::DeleteDC($spaceBlock_DC) + } + + if ($spaceBlock_hBmp -ne [IntPtr]::Zero) + { + $null = [GDI32]::DeleteObject($spaceBlock_hBmp) + } + + if ($dirtyRect_DC -ne [IntPtr]::Zero) + { + $null = [GDI32]::DeleteDC($dirtyRect_DC) + } + + if ($pBitmapInfoHeader -ne [IntPtr]::Zero) + { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($pBitmapInfoHeader) + } + + if ($desktop_DC -ne [IntPtr]::Zero) + { + $null = [User32]::ReleaseDC([User32]::GetDesktopWindow(), $desktop_DC) + } + + # Tangent univers big crunch + for ($y = 0; $y -lt $vertBlockCount; $y++) + { + for ($x = 0; $x -lt $horzBlockCount; $x++) + { + [Runtime.InteropServices.Marshal]::FreeHGlobal($SpaceGrid[$y][$x]) + } + } + } +} + +$global:IngressEventScriptBlock = { + enum MouseFlags { + MOUSEEVENTF_ABSOLUTE = 0x8000 + MOUSEEVENTF_LEFTDOWN = 0x0002 + MOUSEEVENTF_LEFTUP = 0x0004 + MOUSEEVENTF_MIDDLEDOWN = 0x0020 + MOUSEEVENTF_MIDDLEUP = 0x0040 + MOUSEEVENTF_MOVE = 0x0001 + MOUSEEVENTF_RIGHTDOWN = 0x0008 + MOUSEEVENTF_RIGHTUP = 0x0010 + MOUSEEVENTF_WHEEL = 0x0800 + MOUSEEVENTF_XDOWN = 0x0080 + MOUSEEVENTF_XUP = 0x0100 + MOUSEEVENTF_HWHEEL = 0x01000 + } + + enum InputEvent { + Keyboard = 0x1 + MouseClickMove = 0x2 + MouseWheel = 0x3 + KeepAlive = 0x4 + ClipboardUpdated = 0x5 + } + + enum MouseState { + Up = 0x1 + Down = 0x2 + Move = 0x3 + } + + enum ClipboardMode { + Disabled = 1 + Receive = 2 + Send = 3 + Both = 4 + } + + $SM_CXSCREEN = 0 + $SM_CYSCREEN = 1 + + function Set-MouseCursorPos + { + param( + [int] $X = 0, + [int] $Y = 0 + ) + + $x_screen = [User32]::GetSystemMetrics($SM_CXSCREEN) + $y_screen = [User32]::GetSystemMetrics($SM_CYSCREEN) + + [User32]::mouse_event( + [int][MouseFlags]::MOUSEEVENTF_MOVE -bor [int][MouseFlags]::MOUSEEVENTF_ABSOLUTE, + (65535 * $X) / $x_screen, + (65535 * $Y) / $y_screen, + 0, + 0 + ); + + } + + while ($true) + { + try + { + $jsonEvent = $Param.Reader.ReadLine() + } + catch + { + # ($_ | Out-File "c:\temp\debug.txt") + + break + } + + try + { + $aEvent = $jsonEvent | ConvertFrom-Json + } + catch { continue } + + if (-not ($aEvent.PSobject.Properties.name -match "Id")) + { continue } + + switch ([InputEvent] $aEvent.Id) + { + # Keyboard Input Simulation + ([InputEvent]::Keyboard) + { + if ($Param.ViewOnly) + { continue } + + if (-not ($aEvent.PSobject.Properties.name -match "Keys")) + { break } + + [System.Windows.Forms.SendKeys]::SendWait($aEvent.Keys) + + break + } + + # Mouse Move & Click Simulation + ([InputEvent]::MouseClickMove) + { + if ($Param.ViewOnly) + { continue } + + if (-not ($aEvent.PSobject.Properties.name -match "Type")) + { break } + + switch ([MouseState] $aEvent.Type) + { + # Mouse Down/Up + {($_ -eq ([MouseState]::Down)) -or ($_ -eq ([MouseState]::Up))} + { + #[User32]::SetCursorPos($aEvent.X, $aEvent.Y) + Set-MouseCursorPos -X $aEvent.X -Y $aEvent.Y + + $down = ($_ -eq ([MouseState]::Down)) + + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_LEFTDOWN + if (-not $down) + { + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_LEFTUP + } + + switch($aEvent.Button) + { + "Right" + { + if ($down) + { + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_RIGHTDOWN + } + else + { + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_RIGHTUP + } + + break + } + + "Middle" + { + if ($down) + { + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_MIDDLEDOWN + } + else + { + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_MIDDLEUP + } + } + } + [User32]::mouse_event($mouseCode, 0, 0, 0, 0); + + break + } + + # Mouse Move + ([MouseState]::Move) + { + if ($Param.ViewOnly) + { continue } + + #[User32]::SetCursorPos($aEvent.X, $aEvent.Y) + Set-MouseCursorPos -X $aEvent.X -Y $aEvent.Y + + break + } + } + + break + } + + # Mouse Wheel Simulation + ([InputEvent]::MouseWheel) { + if ($Param.ViewOnly) + { continue } + + [User32]::mouse_event([int][MouseFlags]::MOUSEEVENTF_WHEEL, 0, 0, $aEvent.Delta, 0); + + break + } + + # Clipboard Update + ([InputEvent]::ClipboardUpdated) + { + if ($Param.ViewOnly) + { continue } + + if ($Param.Clipboard -eq ([ClipboardMode]::Disabled) -or $Param.Clipboard -eq ([ClipboardMode]::Send)) + { continue } + + if (-not ($aEvent.PSobject.Properties.name -match "Text")) + { continue } + + $HostSyncHash.ClipboardText = $aEvent.Text + + Set-Clipboard -Value $aEvent.Text + } + } + } +} + +$global:EgressEventScriptBlock = { + + enum CursorType { + IDC_APPSTARTING = 32650 + IDC_ARROW = 32512 + IDC_CROSS = 32515 + IDC_HAND = 32649 + IDC_HELP = 32651 + IDC_IBEAM = 32513 + IDC_ICON = 32641 + IDC_NO = 32648 + IDC_SIZE = 32640 + IDC_SIZEALL = 32646 + IDC_SIZENESW = 32643 + IDC_SIZENS = 32645 + IDC_SIZENWSE = 32642 + IDC_SIZEWE = 32644 + IDC_UPARROW = 32516 + IDC_WAIT = 32514 + } + + enum OutputEvent { + KeepAlive = 0x1 + MouseCursorUpdated = 0x2 + ClipboardUpdated = 0x3 + DesktopActive = 0x4 + DesktopInactive = 0x5 + } + + enum ClipboardMode { + Disabled = 1 + Receive = 2 + Send = 3 + Both = 4 + } + + function Initialize-Cursors + { + <# + .SYNOPSIS + Initialize different Windows supported mouse cursors. + + .DESCRIPTION + Unfortunately, there is not WinAPI to get current mouse cursor icon state (Ex: as a flag) + but only current mouse cursor icon (via its handle). + + One solution, is to resolve each supported mouse cursor handles (HCURSOR) with corresponding name + in a hashtable and then compare with GetCursorInfo() HCURSOR result. + #> + $cursors = @{} + + foreach ($cursorType in [CursorType].GetEnumValues()) { + $result = [User32]::LoadCursorA(0, [int]$cursorType) + + if ($result -gt 0) + { + $cursors[[string] $cursorType] = $result + } + } + + return $cursors + } + + function Get-GlobalMouseCursorIconHandle + { + <# + .SYNOPSIS + Return global mouse cursor handle. + .DESCRIPTION + For this project I really want to avoid using "inline c#" but only pure PowerShell Code. + I'm using a Hackish method to retrieve the global Windows cursor info by playing by hand + with memory to prepare and read CURSORINFO structure. + --- + typedef struct tagCURSORINFO { + DWORD cbSize; // Size: 0x4 + DWORD flags; // Size: 0x4 + HCURSOR hCursor; // Size: 0x4 (32bit) , 0x8 (64bit) + POINT ptScreenPos; // Size: 0x8 + } CURSORINFO, *PCURSORINFO, *LPCURSORINFO; + Total Size of Structure: + - [32bit] 20 Bytes + - [64bit] 24 Bytes + #> + + # sizeof(cbSize) + sizeof(flags) + sizeof(ptScreenPos) = 16 + $structSize = [IntPtr]::Size + 16 + + $cursorInfo = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($structSize) + try + { + # ZeroMemory(@cursorInfo, SizeOf(tagCURSORINFO)) + for ($i = 0; $i -lt $structSize; $i++) + { + [System.Runtime.InteropServices.Marshal]::WriteByte($cursorInfo, $i, 0x0) + } + + [System.Runtime.InteropServices.Marshal]::WriteInt32($cursorInfo, 0x0, $structSize) + + if ([User32]::GetCursorInfo($cursorInfo)) + { + $hCursor = [System.Runtime.InteropServices.Marshal]::ReadInt64($cursorInfo, 0x8) + + return $hCursor + } + + <#for ($i = 0; $i -lt $structSize; $i++) + { + $offsetValue = [System.Runtime.InteropServices.Marshal]::ReadByte($cursorInfo, $i) + Write-Host "Offset: ${i} -> " -NoNewLine + Write-Host $offsetValue -ForegroundColor Green -NoNewLine + Write-Host ' (' -NoNewLine + Write-Host ('0x{0:x}' -f $offsetValue) -ForegroundColor Cyan -NoNewLine + Write-Host ')' + }#> + } + finally + { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($cursorInfo) + } + } + + function Send-Event + { + <# + .SYNOPSIS + Send an event to remote peer. + + .PARAMETER AEvent + Type: Enum + Default: None + Description: The event to send to remote viewer. + + .PARAMETER Data + Type: PSCustomObject + Default: None + Description: Additional information about the event. + #> + param ( + [Parameter(Mandatory=$True)] + [OutputEvent] $AEvent, + + [PSCustomObject] $Data = $null + ) + + try + { + if (-not $Data) + { + $Data = New-Object -TypeName PSCustomObject -Property @{ + Id = $AEvent + } + } + else + { + $Data | Add-Member -MemberType NoteProperty -Name "Id" -Value $AEvent + } + + $Param.Writer.WriteLine(($Data | ConvertTo-Json -Compress)) + + return $true + } + catch + { + return $false + } + } + + $cursors = Initialize-Cursors + + $oldCursor = 0 + + # Feature not yet available + #$desktopIsActive = [User32]::GetForegroundWindow() -ne [IntPtr]::Zero + + $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + while ($true) + { + # Events that occurs every seconds needs to be placed bellow. + # If no event has occured during this second we send a Keep-Alive signal to + # remote peer and detect a potential socket disconnection. + if ($stopWatch.ElapsedMilliseconds -ge 1000) + { + try + { + $eventTriggered = $false + + # Clipboard Update Detection + if ( + ($Param.Clipboard -eq ([ClipboardMode]::Both) -or $Param.Clipboard -eq ([ClipboardMode]::Send)) ` + -and (-not $Param.ViewOnly) + ) + { + # IDEA: Check for existing clipboard change event or implement a custom clipboard + # change detector using "WM_CLIPBOARDUPDATE" for example (WITHOUT INLINE C#) + # It is not very important but it would avoid calling "Get-Clipboard" every seconds. + $currentClipboard = (Get-Clipboard -Raw) + + if ($currentClipboard -and $currentClipboard -cne $HostSyncHash.ClipboardText) + { + $data = New-Object -TypeName PSCustomObject -Property @{ + Text = $currentClipboard + } + + if (-not (Send-Event -AEvent ([OutputEvent]::ClipboardUpdated) -Data $data)) + { break } + + $HostSyncHash.ClipboardText = $currentClipboard + + $eventTriggered = $true + } + } + + # Desktop Active / Inactive Detection + <#$desktopActiveProbe = [User32]::GetForegroundWindow() -ne [IntPtr]::Zero + if ($desktopIsActive -ne $desktopActiveProbe) + { + if ($desktopActiveProbe) + { + $aEvent = [OutputEvent]::DesktopActive + } + else + { + $aEvent = [OutputEvent]::DesktopInactive + } + + #if (-not (Send-Event -AEvent $aEvent)) + #{ break } + + $desktopIsActive = $desktopActiveProbe + + #$eventTriggered = $true + }#> + + # Send a Keep-Alive if during this second iteration nothing happened. + if (-not $eventTriggered) + { + if (-not (Send-Event -AEvent ([OutputEvent]::KeepAlive))) + { break } + } + } + finally + { + $stopWatch.Restart() + } + } + + # Monitor for global mouse cursor change + # Update Frequently (Maximum probe time to be efficient: 50ms) + $currentCursor = Get-GlobalMouseCursorIconHandle + if ($currentCursor -ne 0 -and $currentCursor -ne $oldCursor) + { + $cursorTypeName = ($cursors.GetEnumerator() | Where-Object { $_.Value -eq $currentCursor }).Key + + $data = New-Object -TypeName PSCustomObject -Property @{ + Cursor = $cursorTypeName + } + + if (-not (Send-Event -AEvent ([OutputEvent]::MouseCursorUpdated) -Data $data)) + { break } + + $oldCursor = $currentCursor + } + + Start-Sleep -Milliseconds 50 + } + + $stopWatch.Stop() +} + +function New-RunSpace +{ + <# + .SYNOPSIS + Create a new PowerShell Runspace. + + .DESCRIPTION + Notice: the $host variable is used for debugging purpose to write on caller PowerShell + Terminal. + + .PARAMETER ScriptBlock + Type: ScriptBlock + Default: None + Description: Instructions to execute in new runspace. + + .PARAMETER Param + Type: PSCustomObject + Default: None + Description: Object to attach in runspace context. + + .PARAMETER LogonUI + Type: Boolean + Default: False + Description: + New thread will attach its desktop to LogonUI / Winlogon + (Requires SYSTEM Privilege on active session) + + .EXAMPLE + New-RunSpace -Client $newClient -ScriptBlock { Start-Sleep -Seconds 10 } + #> + + param( + [Parameter(Mandatory=$True)] + [ScriptBlock] $ScriptBlock, + + [PSCustomObject] $Param = $null, + [bool] $LogonUI = $false + ) + + $runspace = [RunspaceFactory]::CreateRunspace() + $runspace.ThreadOptions = "UseNewThread" + $runspace.ApartmentState = "STA" + $runspace.Open() + + if ($Param) + { + $runspace.SessionStateProxy.SetVariable("Param", $Param) + } + + $runspace.SessionStateProxy.SetVariable("HostSyncHash", $global:HostSyncHash) + + $powershell = [PowerShell]::Create() + + if ($LogonUI) + { + # Runspace prelude to update new thread desktop before something happens. + # This code will switch new thread desktop from "WinSta0/default" to "WinSta0/winlogon". + # It requires to be "NT AUTHORITY/SYSTEM" + $null = $powershell.AddScript({ + $MAXIMUM_ALLOWED = 0x02000000; + + $winLogonDesktop = [User32]::OpenDesktop("winlogon", 0, $false, $MAXIMUM_ALLOWED); + if ($winLogonDesktop -eq [IntPtr]::Zero) + { + return + } + + if (-not [User32]::SetThreadDesktop($winLogonDesktop)) + { + [User32]::CloseDesktop($winLogonDesktop) + + return + } + }) + } + + $null = $powershell.AddScript($ScriptBlock) + + $powershell.Runspace = $runspace + + $asyncResult = $powershell.BeginInvoke() + + return New-Object PSCustomObject -Property @{ + Runspace = $runspace + PowerShell = $powershell + AsyncResult = $asyncResult + } +} + +class ClientIO { + [System.Net.Sockets.TcpClient] $Client = $null + [System.IO.StreamWriter] $Writer = $null + [System.IO.StreamReader] $Reader = $null + [System.Net.Security.SslStream] $SSLStream = $null + + + ClientIO( + [System.Net.Sockets.TcpClient] $Client, + [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, + [bool] $UseTLSv1_3 + ) { + if ((-not $Client) -or (-not $Certificate)) + { + throw "ClientIO Class requires both a valid TcpClient and X509Certificate2." + } + + $this.Client = $Client + + Write-Verbose "Create new SSL Stream..." + + $this.SSLStream = New-Object System.Net.Security.SslStream($this.Client.GetStream(), $false) + + if ($UseTLSv1_3) + { + $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS13 + } + else { + $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS12 + } + + Write-Verbose "Authenticate as server using ${TLSVersion}..." + + $this.SSLStream.AuthenticateAsServer( + $Certificate, + $false, + $TLSVersion, + $false + ) + + if (-not $this.SSLStream.IsEncrypted) + { + throw "Could not established an encrypted tunnel with remote peer." + } + + $this.SSLStream.WriteTimeout = 5000 + $this.SSLStream.ReadTimeout = [System.Threading.Timeout]::Infinite # Default + + Write-Verbose "Open communication channels..." + + $this.Writer = New-Object System.IO.StreamWriter($this.SSLStream) + $this.Writer.AutoFlush = $true + + $this.Reader = New-Object System.IO.StreamReader($this.SSLStream) + + Write-Verbose "Connection ready for use." + } + + [bool] Authentify([SecureString] $SecurePassword) { + <# + .SYNOPSIS + Handle authentication process with remote peer. + + .PARAMETER Password + Type: SecureString + Default: None + Description: Secure String object containing the password. + + .EXAMPLE + .Authentify((ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force)) + #> + try + { + if (-not $SecurePassword) { + throw "During client authentication, a password cannot be blank." + } + + Write-Verbose "New authentication challenge..." + + $candidate = -join ((1..128) | ForEach-Object {Get-Random -input ([char[]](33..126))}) + $candidate = Get-SHA512FromString -String $candidate + + $challengeSolution = Resolve-AuthenticationChallenge -Candidate $candidate -SecurePassword $SecurePassword + + Write-Verbose "@Challenge:" + Write-Verbose "Candidate: ""${candidate}""" + Write-Verbose "Solution: ""${challengeSolution}""" + Write-Verbose "---" + + $this.Writer.WriteLine($candidate) + + Write-Verbose "Candidate sent to client, waiting for answer..." + + $challengeReply = $this.ReadLine(5 * 1000) + + Write-Verbose "Replied solution: ""${challengeReply}""" + + # Challenge solution is a Sha512 Hash so comparison doesn't need to be sensitive (-ceq or -cne) + if ($challengeReply -ne $challengeSolution) + { + $this.Writer.WriteLine(([ProtocolCommand]::Fail)) + + throw "Client challenge solution does not match our solution." + } + else + { + $this.Writer.WriteLine(([ProtocolCommand]::Success)) + + Write-Verbose "Password Authentication Success" + + return 280121 # True + } + } + catch + { + throw "Password Authentication Failed. Reason: `r`n $($_)" + } + } + + [string] RemoteAddress() { + return $this.Client.Client.RemoteEndPoint.Address + } + + [int] RemotePort() { + return $this.Client.Client.RemoteEndPoint.Port + } + + [string] LocalAddress() { + return $this.Client.Client.LocalEndPoint.Address + } + + [int] LocalPort() { + return $this.Client.Client.LocalEndPoint.Port + } + + [string] ReadLine([int] $Timeout) + { + <# + .SYNOPSIS + Read string message from remote peer with timeout support. + + .PARAMETER Timeout + Type: Integer + Description: Maximum period of time to wait for incomming data. + #> + $defautTimeout = $this.SSLStream.ReadTimeout + try + { + $this.SSLStream.ReadTimeout = $Timeout + + return $this.Reader.ReadLine() + } + finally + { + $this.SSLStream.ReadTimeout = $defautTimeout + } + } + + [string] ReadLine() + { + <# + .SYNOPSIS + Shortcut to Reader ReadLine method. No timeout support. + #> + return $this.Reader.ReadLine() + } + + [void] WriteJson([PSCustomObject] $Object) + { + <# + .SYNOPSIS + Transform a PowerShell Object as a JSON Representation then send to remote + peer. + + .PARAMETER Object + Type: PSCustomObject + Description: Object to be serialized in JSON. + #> + + $this.Writer.WriteLine(($Object | ConvertTo-Json -Compress)) + } + + [void] WriteLine([string] $Value) + { + $this.Writer.WriteLine($Value) + } + + [void] Close() { + <# + .SYNOPSIS + Release streams and client. + #> + + if ($this.Writer) + { + $this.Writer.Close() + } + + if ($this.Reader) + { + $this.Reader.Close() + } + + if ($this.Stream) + { + $this.Stream.Close() + } + + if ($this.Client) + { + $this.Client.Close() + } + } +} + +class TcpListenerEx : System.Net.Sockets.TcpListener +{ + TcpListenerEx([string] $ListenAddress, [int] $ListenPort) : base($ListenAddress, $ListenPort) + { } + + [bool] Active() + { + return $this.Active + } +} + +class ServerIO { + [TcpListenerEx] $Server = $null + [System.IO.StreamWriter] $Writer = $null + [System.IO.StreamReader] $Reader = $null + + ServerIO() + { } + + [void] Listen( + [string] $ListenAddress, + [int] $ListenPort + ) + { + if ($this.Server) + { + $this.Close() + } + + $this.Server = New-Object TcpListenerEx( + $ListenAddress, + $ListenPort + ) + + $this.Server.Start() + + Write-Verbose "Listening on ""$($ListenAddress):$($ListenPort)""..." + } + + [ClientIO] PullClient( + [SecureString] $SecurePassword, + + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $Certificate, + + [bool] $UseTLSv13, + [int] $Timeout + ) { + <# + .SYNOPSIS + Accept new client and associate this client with a new ClientIO Object. + + .PARAMETER Timeout + Type: Integer + Description: + By default AcceptTcpClient() will block current thread until a client connects. + + Using Timeout and a cool technique, you can stop waiting for client after a certain amount + of time (In Milliseconds) + + If Timeout is greater than 0 (Milliseconds) then connection timeout is enabled. + + Other method: AsyncWaitHandle.WaitOne([timespan])'h:m:s') -eq $true|$false with BeginAcceptTcpClient(...) + #> + + if (-not (Test-PasswordComplexity -SecurePasswordCandidate $SecurePassword)) + { + throw "Client socket pull request requires a complex password to be set." + } + + if ($Timeout -gt 0) + { + $socketReadList = [System.Collections.ArrayList]@($this.Server.Server) + + [System.Net.Sockets.Socket]::Select($socketReadList, $null, $null, $Timeout * 1000) + + if (-not $socketReadList.Contains($this.Server.Server)) + { + throw "Pull timeout." + } + } + + $socket = $this.Server.AcceptTcpClient() + + $client = [ClientIO]::New( + $socket, + $Certificate, + $UseTLSv13 + ) + try + { + Write-Verbose "New client socket connected from: ""$($client.RemoteAddress())""." + + $authenticated = ($client.Authentify($SecurePassword) -eq 280121) + if (-not $authenticated) + { + throw "Access Denied." + } + } + catch + { + $client.Close() + + throw $_ + } + + return $client + } + + [bool] Active() + { + if ($this.Server) + { + return $this.Server.Active() + } + else + { + return $false + } + } + + [void] Close() + { + <# + .SYNOPSIS + Stop listening and release TcpListener object. + #> + if ($this.Server) + { + if ($this.Server.Active) + { + $this.Server.Stop() + } + + $this.Server = $null + + Write-Verbose "Server is now released." + } + } +} + +class ServerSession { + [string] $Id = "" + [bool] $ViewOnly = $false + [ClipboardMode] $Clipboard = [ClipboardMode]::Both + [string] $ViewerLocation = "" + [bool] $LogonUI = $false + + [System.Collections.Generic.List[PSCustomObject]] + $WorkerThreads = @() + + [System.Collections.Generic.List[ClientIO]] + $Clients = @() + + $SafeHash = [HashTable]::Synchronized(@{ + SessionActive = $true + }) + + ServerSession( + [bool] $ViewOnly, + [ClipboardMode] $Clipboard, + [string] $ViewerLocation + ) + { + $this.Id = (SHA512FromString -String (-join ((1..128) | ForEach-Object {Get-Random -input ([char[]](33..126))}))) + + $this.ViewOnly = $ViewOnly + $this.Clipboard = $Clipboard + $this.ViewerLocation = $ViewerLocation + } + + [bool] CompareSession([string] $Id) + { + <# + .SYNOPSIS + Compare two session object. In this case just compare session id string. + + .PARAMETER Id + Type: String + Description: A session id to compare with current session object. + #> + return ($this.Id -ceq $Id) + } + + [void] NewDesktopWorker([ClientIO] $Client) + { + <# + .SYNOPSIS + Create a new desktop streaming worker (Runspace/Thread). + + .PARAMETER Client + Type: ClientIO + Description: Established connection with a remote peer. + #> + $param = New-Object -TypeName PSCustomObject -Property @{ + Client = $Client + SafeHash = $this.SafeHash + } + + $this.WorkerThreads.Add((New-RunSpace -ScriptBlock $global:DesktopStreamScriptBlock -Param $param -LogonUI $this.LogonUI)) + + ### + + $this.Clients.Add($Client) + } + + [void] NewEventWorker([ClientIO] $Client) + { + <# + .SYNOPSIS + Create a new egress / ingress worker (Runspace/Thread) to process outgoing / incomming events. + + .PARAMETER Client + Type: ClientIO + Description: Established connection with a remote peer. + #> + + $param = New-Object -TypeName PSCustomObject -Property @{ + Writer = $Client.Writer + Clipboard = $this.Clipboard + SafeHash = $this.SafeHash + } + + $this.WorkerThreads.Add((New-RunSpace -ScriptBlock $global:EgressEventScriptBlock -Param $param -LogonUI $this.LogonUI)) + + ### + + $param = New-Object -TypeName PSCustomObject -Property @{ + Reader = $Client.Reader + Clipboard = $this.Clipboard + ViewOnly = $this.ViewOnly + SafeHash = $this.SafeHash + } + + $this.WorkerThreads.Add((New-RunSpace -ScriptBlock $global:IngressEventScriptBlock -Param $param -LogonUI $this.LogonUI)) + + ### + + $this.Clients.Add($Client) + } + + [void] CheckSessionIntegrity() + { + <# + .SYNOPSIS + Check if session integrity is still respected. + + .DESCRIPTION + We consider that a dead session, is a session with at least one worker that has completed his + tasks. + + This will notify other workers that something happened (disconnection, fatal exception). + #> + + foreach ($worker in $this.WorkerThreads) + { + if ($worker.AsyncResult.IsCompleted) + { + $this.Close() + + break + } + } + } + + [void] Close() + { + <# + .SYNOPSIS + Close components associated with current session (Ex: runspaces, sockets etc..) + #> + + Write-Verbose "Closing session..." + + $this.SafeHash.SessionActive = $false + + Write-Verbose "Close associated peers..." + + # Close connection with remote peers associated with this session + foreach ($client in $this.Clients) + { + $client.Close() + } + + $this.Clients.Clear() + + Write-Verbose "Wait for associated threads to finish their tasks..." + + while ($true) + { + $completed = $true + + foreach ($worker in $this.WorkerThreads) + { + if (-not $worker.AsyncResult.IsCompleted) + { + $completed = $false + + break + } + } + + if ($completed) + { break } + + Start-Sleep -Seconds 1 + } + + Write-Verbose "Dispose threads (runspaces)..." + + # Terminate runspaces associated with this session + foreach ($worker in $this.WorkerThreads) + { + $null = $worker.PowerShell.EndInvoke($worker.AsyncResult) + $worker.PowerShell.Runspace.Dispose() + $worker.PowerShell.Dispose() + } + $this.WorkerThreads.Clear() + + Write-Host "Session terminated with viewer: $($this.ViewerLocation)" + + Write-Verbose "Session closed." + } +} + +class SessionManager { + [ServerIO] $Server = $null + + [System.Collections.Generic.List[ServerSession]] + $Sessions = @() + + [SecureString] $SecurePassword = $null + + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $Certificate = $null + + [bool] $ViewOnly = $false + [bool] $UseTLSv13 = $false + + [ClipboardMode] $Clipboard = [ClipboardMode]::Both + + SessionManager( + [SecureString] $SecurePassword, + + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $Certificate, + + [bool] $ViewOnly, + [bool] $UseTLSv13, + [ClipboardMode] $Clipboard + ) + { + Write-Verbose "Initialize new session manager..." + + $this.SecurePassword = $SecurePassword + $this.ViewOnly = $ViewOnly + $this.UseTLSv13 = $UseTLSv13 + $this.Clipboard = $Clipboard + + if (-not $Certificate) + { + Write-Verbose "No custom certificate specified, using default X509 Certificate (Not Recommended)." + + $this.Certificate = Get-DefaultCertificateOrCreate + } + else + { + $this.Certificate = $Certificate + } + + Write-Verbose "@Certificate:" + Write-Verbose $this.Certificate + Write-Verbose "---" + + Write-Verbose "Session manager initialized." + } + + [void] OpenServer( + [string] $ListenAddress, + [int] $ListenPort + ) + { + <# + .SYNOPSIS + Create a new server object then start listening on desired interface / port. + + .PARAMETER ListenAddress + Desired interface to listen for new peers. + "127.0.0.1" = Only listen for localhost peers. + "0.0.0.0" = Listen on all interfaces for peers. + + .PARAMETER ListenPort + TCP Port to listen for new peers (0-65535) + #> + + $this.CloseServer() + try + { + $this.Server = [ServerIO]::New() + + $this.Server.Listen( + $ListenAddress, + $ListenPort + ) + } + catch + { + $this.CloseServer() + + throw $_ + } + } + + [ServerSession] GetSession([string] $SessionId) + { + <# + .SYNOPSIS + Find a session by its id on current session pool. + + .PARAMETER SessionId + Type: String + Description: SessionId to retrieve from session pool. + #> + foreach ($session in $this.Sessions) + { + if ($session.CompareSession($SessionId)) + { + return $session + } + } + + return $null + } + + [void] ProceedNewSessionRequest([ClientIO] $Client) + { + <# + .SYNOPSIS + Attempt a new session request with remote peer. + + .DESCRIPTION + Session creation is now requested from a dedicated client instead of using + same client as for desktop streaming. + + I prefer to use a dedicated client to have a more cleaner session establishement + process. + + Session request will basically generate a new session object, send some information + about current server marchine state then wait for viewer acknowledgement with desired + configuration (Ex: desired screen to capture, quality and local size constraints). + + When session creation is done, client is then closed. + #> + try + { + Write-Verbose "Remote peer as requested a new session..." + + $session = [ServerSession]::New( + $this.ViewOnly, + $this.Clipboard, + $client.RemoteAddress() + ) + + Write-Verbose "@ServerSession" + Write-Verbose "Id: ""$($session.Id)""" + Write-Verbose "---" + + $serverInformation = New-Object PSCustomObject -Property @{ + # Session information and configuration + SessionId = $session.Id + Version = $global:ArcaneProtocolVersion + ViewOnly = $this.ViewOnly + Clipboard = $this.Clipboard + + # Local machine information + MachineName = [Environment]::MachineName + Username = [Environment]::UserName + WindowsVersion = [Environment]::OSVersion.VersionString + } + + Write-Verbose "Sending server information to remote peer..." + + Write-Verbose "@ServerInformation:" + Write-Verbose $serverInformation + Write-Verbose "---" + + $client.WriteJson($serverInformation) + + Write-Verbose "New session successfully created." + + $this.Sessions.Add($session) + + $client.WriteLine(([ProtocolCommand]::Success)) + } + catch + { + $session = $null + + throw $_ + } + finally + { + if ($client) + { + $client.Close() + } + } + } + + [void] ProceedAttachRequest([ClientIO] $Client) + { + <# + .SYNOPSIS + Attach a new peer to an existing session then dispatch this new peer as a + new stateful worker. + + .PARAMETER Client + An established connection with remote peer as a ClientIO Object. + #> + Write-Verbose "Proceed new session attach request..." + + $session = $this.GetSession($Client.ReadLine(5 * 1000)) + if (-not $session) + { + $Client.WriteLine(([ProtocolCommand]::ResourceNotFound)) + + throw "Could not locate session." + } + + Write-Verbose "Client successfully attached to session: ""$($session.id)""" + + $Client.WriteLine(([ProtocolCommand]::ResourceFound)) + + $workerKind = $Client.ReadLine(5 * 1000) + + switch ([WorkerKind] $workerKind) + { + (([WorkerKind]::Desktop)) + { + $session.NewDesktopWorker($Client) + + break + } + + (([WorkerKind]::Events)) + { + $session.NewEventWorker($Client) # I/O + + break + } + } + } + + [void] ListenForWorkers() + { + <# + .SYNOPSIS + Process server client queue and dispatch accordingly. + #> + while ($true) + { + if (-not $this.Server -or -not $this.Server.Active()) + { + throw "A server must be active to listen for new workers." + } + + try + { + $this.CheckSessionsIntegrity() + } + catch + { } + + $client = $null + try + { + $client = $this.Server.PullClient( + $this.SecurePassword, + $this.Certificate, + $this.UseTLSv13, + 5 * 1000 + ) + + $requestMode = $client.ReadLine(5 * 1000) + + switch ([ProtocolCommand] $requestMode) + { + ([ProtocolCommand]::RequestSession) + { + $remoteAddress = $client.RemoteAddress() + + $this.ProceedNewSessionRequest($client) + + Write-Host "New remote desktop session established with: $($remoteAddress)" + + break + } + + ([ProtocolCommand]::AttachToSession) + { + $this.ProceedAttachRequest($client) + + break + } + + default: + { + $client.WriteLine(([ProtocolCommand]::BadRequest)) + + throw "Bad request." + } + } + } + catch + { + if ($client) + { + $client.Close() + + $client = $null + } + } + finally + { } + } + } + + [void] CheckSessionsIntegrity() + { + <# + .SYNOPSIS + Check if existing server sessions integrity is respected. + Use this method to free dead/half-dead sessions. + #> + foreach ($session in $this.Sessions) + { + $session.CheckSessionIntegrity() + } + } + + [void] CloseSessions() + { + <# + .SYNOPSIS + Terminate existing server sessions. + #> + + foreach ($session in $this.Sessions) + { + $session.Close() + } + + $this.Sessions.Clear() + } + + [void] CloseServer() + { + <# + .SYNOPSIS + Terminate existing server sessions then release server. + #> + + $this.CloseSessions() + + if ($this.Server) + { + $this.Server.Close() + + $this.Server = $null + } + } +} + +function Test-Administrator +{ + <# + .SYNOPSIS + Check if current user is administrator. + #> + $windowsPrincipal = New-Object Security.Principal.WindowsPrincipal( + [Security.Principal.WindowsIdentity]::GetCurrent() + ) + + return $windowsPrincipal.IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator + ) +} + +class ValidateFileAttribute : System.Management.Automation.ValidateArgumentsAttribute +{ + <# + .SYNOPSIS + Check if file argument exists on disk. + #> + + [void]Validate([System.Object] $arguments, [System.Management.Automation.EngineIntrinsics] $engineIntrinsics) + { + if(-not (Test-Path -Path $arguments)) + { + throw [System.IO.FileNotFoundException]::new() + } + } +} + +class ValidateBase64StringAttribute : System.Management.Automation.ValidateArgumentsAttribute +{ + <# + .SYNOPSIS + Check if string argument is a valid Base64 String. + #> + + [void]Validate([System.Object] $arguments, [System.Management.Automation.EngineIntrinsics] $engineIntrinsics) + { + [Convert]::FromBase64String($arguments) + } +} + +function Invoke-ArcaneServer +{ + <# + .SYNOPSIS + Create and start a new Arcane Server. + + .DESCRIPTION + Notices: + + 1- Prefer using SecurePassword over plain-text password even if a plain-text password is getting converted to SecureString anyway. + + 2- Not specifying a custom certificate using CertificateFile or EncodedCertificate result in generating a default + self-signed certificate (if not already generated) that will get installed on local machine thus requiring administrator privilege. + If you want to run the server as a non-privileged account, specify your own certificate location. + + 3- If you don't specify a SecurePassword or Password, a random complex password will be generated and displayed on terminal + (this password is temporary) + + .PARAMETER ListenAddress + Type: String + Default: 0.0.0.0 + Description: IP Address that represents the local IP address. + + .PARAMETER ListenPort + Type: Integer + Default: 2801 (0 - 65535) + Description: The port on which to listen for incoming connection. + + .PARAMETER SecurePassword + Type: SecureString + Default: None + Description: SecureString object containing password used to authenticate remote viewer (Recommended) + + .PARAMETER Password + Type: String + Default: None + Description: Plain-Text Password used to authenticate remote viewer (Not recommended, use SecurePassword instead) + + .PARAMETER CertificateFile + Type: String + Default: None + Description: A file containing valid certificate information (x509), must include the private key. + + .PARAMETER EncodedCertificate + Type: String (Base64 Encoded) + Default: None + Description: A base64 representation of the whole certificate file, must include the private key. + + .PARAMETER UseTLSv1_3 + Type: Switch + Default: False + Description: If present, TLS v1.3 will be used instead of TLS v1.2 (Recommended if applicable to both systems) + + .PARAMETER DisableVerbosity + Type: Switch + Default: False + Description: If present, program wont show verbosity messages. + + .PARAMETER Clipboard + Type: Enum + Default: Both + Description: + Define clipboard synchronization mode (Both, Disabled, Send, Receive) see bellow for more detail. + + * Disabled -> Clipboard synchronization is disabled in both side + * Receive -> Only incomming clipboard is allowed + * Send -> Only outgoing clipboard is allowed + * Both -> Clipboard synchronization is allowed on both side + + .PARAMETER ViewOnly (Default: None) + Type: Swtich + Default: False + Description: If present, remote viewer is only allowed to view the desktop (Mouse and Keyboard are not authorized) + + .PARAMETER PreventComputerToSleep + Type: Switch + Default: False + Description: If present, this option will prevent computer to enter in sleep mode while server is active and waiting for new connections. + + .PARAMETER CertificatePassword + Type: SecureString + Default: None + Description: Specify the password used to open a password-protected x509 Certificate provided by user. + + .EXAMPLE + Invoke-ArcaneServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) + Invoke-ArcaneServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) -CertificateFile "c:\certs\phrozen.p12" + #> + + param ( + [string] $ListenAddress = "0.0.0.0", + + [ValidateRange(0, 65535)] + [int] $ListenPort = 2801, + + [SecureString] $SecurePassword = $null, + [string] $Password = "", + [String] $CertificateFile = $null, + [string] $EncodedCertificate = "", + [switch] $UseTLSv1_3, + [switch] $DisableVerbosity, + [ClipboardMode] $Clipboard = [ClipboardMode]::Both, + [switch] $ViewOnly, + [switch] $PreventComputerToSleep, + [SecureString] $CertificatePassword = $null + ) + + $oldErrorActionPreference = $ErrorActionPreference + $oldVerbosePreference = $VerbosePreference + try + { + $ErrorActionPreference = "stop" + + if (-not $DisableVerbosity) + { + $VerbosePreference = "continue" + } + else + { + $VerbosePreference = "SilentlyContinue" + } + + Write-Banner + + $null = [User32]::SetProcessDPIAware() + + $Certificate = $null + + if ($CertificateFile -or $EncodedCertificate) + { + $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + try + { + if ($CertificateFile) + { + if(-not (Test-Path -Path $CertificateFile)) + { + throw [System.IO.FileNotFoundException]::new() + } + + $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $CertificateFile, $CertificatePassword + } + else + { + $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 ([Convert]::FromBase64String($EncodedCertificate)), $CertificatePassword + } + } + catch + { + $message = "Could not open provided x509 Certificate. Possible Reasons:`r`n" + + "* Provided certificate is not a valid x509 Certificate.`r`n" + + "* Certificate is corrupted.`r`n" + + if (-not $CertificatePassword) + { + $message += "* Certificate is protected by a password.`r`n" + } + else + { + $message += "* Provided certificate password is not valid.`r`n" + } + + $message += "More detail: $($_)" + + throw $message + } + + if (-not $Certificate.HasPrivateKey) + { + throw "Provided Certificate must have private-key included." + } + } + + # If plain-text password is set, we convert this password to a secured representation. + if ($Password -and -not $SecurePassword) + { + $SecurePassword = (ConvertTo-SecureString -String $Password -AsPlainText -Force) + } + + if (-not $SecurePassword) + { + $SecurePassword = New-RandomPassword + + Write-Host -NoNewLine "Server password: """ + Write-Host -NoNewLine $(Get-PlainTextPassword -SecurePassword $SecurePassword) -ForegroundColor green + Write-Host """." + } + else + { + if (-not (Test-PasswordComplexity -SecurePasswordCandidate $SecurePassword)) + { + throw "Password complexity is too weak. Please choose a password following following rules:`r`n` + * Minimum 12 Characters`r`n` + * One of following symbols: ""!@#%^&*_""`r`n` + * At least of lower case character`r`n` + * At least of upper case character`r`n" + } + } + + Remove-Variable -Name "Password" -ErrorAction SilentlyContinue + + try + { + $oldExecutionStateFlags = $null + if ($PreventComputerToSleep) + { + $oldExecutionStateFlags = Invoke-PreventSleepMode + + Write-OperationSuccessState -Message "Preventing computer to entering sleep mode." -Result ($oldExecutionStateFlags -gt 0) + } + + Write-Host "Loading remote desktop server components..." + + $sessionManager = [SessionManager]::New( + $SecurePassword, + $Certificate, + $ViewOnly, + $UseTLSv1_3, + $Clipboard + ) + + $sessionManager.OpenServer( + $ListenAddress, + $ListenPort + ) + + Write-Host "Server is ready to receive new connections..." + + $sessionManager.ListenForWorkers() + } + finally + { + if ($sessionManager) + { + $sessionManager.CloseServer() + + $sessionManager = $null + } + + if ($oldExecutionStateFlags) + { + Write-OperationSuccessState -Message "Stop preventing computer to enter sleep mode. Restore thread execution state." -Result (Update-ThreadExecutionState -Flags $oldExecutionStateFlags) + } + + Write-Host "Remote desktop was closed." + } + } + finally + { + $ErrorActionPreference = $oldErrorActionPreference + $VerbosePreference = $oldVerbosePreference + } +} + +try { + Export-ModuleMember -Function Invoke-ArcaneServer +} catch {} \ No newline at end of file diff --git a/README.md b/README.md index 5ef00fc..9cb8274 100644 --- a/README.md +++ b/README.md @@ -1 +1,127 @@ -# ArcaneServer \ No newline at end of file +# Arcane Server + +![Banner](assets/imgs/banner.png) + +This repository contains the Arcane Server component of the Arcane project, which is fully implemented in PowerShell. It operates independently without relying on any third-party software such as RDP or VNC. Instead, it leverages the native Windows API, using the full capabilities of PowerShell. + +> ⓘ Since version 1.0.4, the Arcane Viewer and Server have separate versioning, allowing each to progress independently. This separation ensures that if only Viewer features are optimized, enhanced, or bug-fixed, the Server version doesn't need to be updated unnecessarily (and vis-versa). Although having different versions for the Viewer and Server might seem confusing, the key detail to focus on is the protocol version. The protocol version determines compatibility between the Viewer and Server, ensuring they work together correctly. + +## Quick Setup (Latest Release) - [PowerShell Gallery](https://www.powershellgallery.com) + +> ⚠️ Please note that you must have administrative privileges to install a new PowerShell module. + +Open an elevated PowerShell prompt and execute the following command: + +```powershell +Install-Module -Name Arcane_Server +``` + +The latest version of the Arcane Server should now be installed and available. + +Before running the server, you must import the module into your current PowerShell session, note that it is now mandatory to have an elevated PowerShell session, Arcane Server support both running as limited and privileged user, however, if session is running with limited privilege, mouse and keyboard wont be able to be captured for elevated window's + +> ⓘ depending on your system configuration, you may need to run the following command to temporarily bypass the execution policy in order to run an unsigned script: +> `powershell.exe -executionpolicy bypass` + +```powershell +Import-Module Arcane_Server +``` + +Once the module is imported, you can run the server using the following command: + +```powershell +Invoke-ArcaneServer +``` + +That's it, you're ready to go! 🚀 + +## Version Table + +| Version | Protocol Version | Release Date | +|-----------------|------------------|-----------------| +| 1.0.4 | 5.0.1 | 15 August 2024 | + +## Advanced Usage + +```powershell +Invoke-ArcaneServer +``` + +### Supported Options: + +| Parameter | Type | Default | Description | +|------------------------|------------------|------------|--------------| +| ServerAddress | String | 0.0.0.0 | IP address representing the local machine's IP address | +| ServerPort | Integer | 2801 | The port number on which to listen for incoming connections | +| SecurePassword | SecureString | None | SecureString object containing the password used for authenticating remote viewers (recommended) | +| Password | String | None | Plain-text password used for authenticating remote viewers (not recommended; use SecurePassword instead) | +| DisableVerbosity | Switch | False | If specified, the program will suppress verbosity messages | +| UseTLSv1_3 | Switch | False | If specified, the program will use TLS v1.3 instead of TLS v1.2 for encryption (recommended if both systems support it) | +| Clipboard | Enum | Both | Specify the clipboard synchronization mode (options include 'Both', 'Disabled', 'Send', and 'Receive'; see below for more detail) | +| CertificateFile | String | None | A file containing valid certificate information (x509) that includes the private key | +| EncodedCertificate | String | None | A base64-encoded representation of the entire certificate file, including the private key | +| ViewOnly | Switch | False | If specified, the remote viewer will only be able to view the desktop and will not have access to the mouse or keyboard | +| PreventComputerToSleep | Switch | False | If specified, this option will prevent the computer from entering sleep mode while the server is active and waiting for new connections | +| CertificatePassword | SecureString | None | Specify the password used to access a password-protected x509 certificate provided by the user | + +### Server Address Examples + +| Value | Description | +|-------------------|--------------------------------------------------------------------------| +| 127.0.0.1 | Only listen for connections from the localhost (usually for debugging purposes) | +| 0.0.0.0 | Listen for connections on all network interfaces, including the local network and the internet | + +### Clipboard Mode Enum Properties + +| Value | Description | +|-------------------|----------------------------------------------------| +| Disabled | Clipboard synchronization is disabled on both the viewer and server sides | +| Receive | Only incoming clipboard data is allowed | +| Send | Only outgoing clipboard data is allowed | +| Both | Clipboard synchronization is allowed on both the viewer and server sides | + +### ⚠️ Important Notices + +1. It is recommended to use SecurePassword instead of a plain-text password, even if the plain-text password is being converted to a SecureString. +2. If you do not specify a custom certificate using 'CertificateFile' or 'EncodedCertificate', a default self-signed certificate will be generated and installed for the local user. +3. If you do not specify a SecurePassword or Password, a random, complex password will be generated and displayed in the terminal (this password is temporary). + +### Examples + +```powershell +Invoke-ArcaneServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) + +Invoke-ArcaneServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) -CertificateFile "c:\certs\phrozen.p12" +``` + +### Generate your Certificate + +``` +openssl req -x509 -sha512 -nodes -days 365 -newkey rsa:4096 -keyout phrozen.key -out phrozen.crt +``` + +Then export the new certificate (**must include private key**). + +``` +openssl pkcs12 -export -out phrozen.p12 -inkey phrozen.key -in phrozen.crt +``` + +### Integrate to server as a file + +Use `CertificateFile`. Example: `c:\tlscert\phrozen.crt` + +### Integrate to server as a base64 representation + +Encode an existing certificate using PowerShell + +```powershell +[convert]::ToBase64String((Get-Content -path "c:\tlscert\phrozen.crt" -Encoding byte)) +``` +or on Linux / Mac systems + +``` +base64 -i /tmp/phrozen.p12 +``` + +You can then pass the output base64 certificate file to parameter `EncodedCertificate` (One line) + diff --git a/TestServer.ps1 b/TestServer.ps1 new file mode 100644 index 0000000..f86c8e7 --- /dev/null +++ b/TestServer.ps1 @@ -0,0 +1,81 @@ +# cd Desktop\Arcane\Server\; IEX (Get-Content .\TestServer.ps1 -Raw -Encoding UTF8) + +Write-Output "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️" +Write-Output "⚠️ Only use this script for testing the application NOT in production ⚠️" +Write-Output "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️" + +Invoke-Expression -Command (Get-Content "Arcane_Server.psm1" -Raw) + +$password = "Jade@123@Pwd" +$encodedCertificate = "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" +$pwdProtectedEncodedCertificate = "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" + +Write-Host "Scenarios" +Write-Host "---------------" +Write-Host "1. Classic (Secure Password)" +Write-Host "2. Classic (Plain-text password)" +Write-Host "3. Classic Default Certificate" +Write-Host "4. TLS v1.3" +Write-Host "5. Verbosity Disabled, View Only, Prevent computer to sleep" +Write-Host "6. Receive clipboard only" +Write-Host "7. Send clipboard only" +Write-Host "8. Clipboard synchronization disabled" + +Write-Host "" + +[int]$scenario = Read-Host "Please choose scenario (default: 1)" + +switch ($scenario) +{ + 2 + { + Invoke-ArcaneServer -Password $password -EncodedCertificate $pwdProtectedEncodedCertificate -CertificatePassword (ConvertTo-SecureString -String "hello" -AsPlainText -Force) + } + + 3 + { + Invoke-ArcaneServer -Password $password + } + + 4 + { + Write-Host "⚡Check that TLSv1.3 is working." + + Invoke-ArcaneServer -Password $password -UseTLSv1_3 + } + + 5 + { + Write-Host "⚡Check that verbosity is disabled." + Write-Host "⚡Check that remote viewer can't control mouse and keyboard." + Write-Host "⚡Check that computer wont go to sleep." + + Invoke-ArcaneServer -Password $password -DisableVerbosity -ViewOnly -PreventComputerToSleep + } + + 6 + { + Write-Host "⚡Check if server is only authorized to receive remote clipboard." + + Invoke-ArcaneServer -Password $password -EncodedCertificate $encodedCertificate -Clipboard "Receive" + } + + 7 + { + Write-Host "⚡Check if server is only authorized to send local clipboard." + + Invoke-ArcaneServer -Password $password -EncodedCertificate $encodedCertificate -Clipboard "Send" + } + + 8 + { + Write-Host "⚡Check if clipboard synchronization is completely disabled." + + Invoke-ArcaneServer -Password $password -EncodedCertificate $encodedCertificate -Clipboard "Disabled" + } + + default + { + Invoke-ArcaneServer -SecurePassword (ConvertTo-SecureString -String $password -AsPlainText -Force) -EncodedCertificate $encodedCertificate + } +} \ No newline at end of file diff --git a/assets/imgs/banner.png b/assets/imgs/banner.png new file mode 100644 index 0000000..3e7ec9a Binary files /dev/null and b/assets/imgs/banner.png differ diff --git a/deprecated/old project viewer/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psd1 b/deprecated/old project viewer/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psd1 new file mode 100644 index 0000000..181e4d3 Binary files /dev/null and b/deprecated/old project viewer/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psd1 differ diff --git a/deprecated/old project viewer/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 b/deprecated/old project viewer/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 new file mode 100644 index 0000000..2a2df8d --- /dev/null +++ b/deprecated/old project viewer/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 @@ -0,0 +1,2213 @@ +<#------------------------------------------------------------------------------- + + Power Remote Desktop + + In loving memory of my father. + Thanks for all you've done. + you will remain in my heart forever. + + .Developer + Jean-Pierre LESUEUR (@DarkCoderSc) + https://www.twitter.com/darkcodersc + https://github.com/DarkCoderSc + www.phrozen.io + jplesueur@phrozen.io + PHROZEN + + .License + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + .Disclaimer + We are doing our best to prepare the content of this app. However, PHROZEN SASU and / or + Jean-Pierre LESUEUR cannot warranty the expressions and suggestions of the contents, + as well as its accuracy. In addition, to the extent permitted by the law, + PHROZEN SASU and / or Jean-Pierre LESUEUR shall not be responsible for any losses + and/or damages due to the usage of the information on our app. + + By using our app, you hereby consent to our disclaimer and agree to its terms. + + Any links contained in our app may lead to external sites are provided for + convenience only. Any information or statements that appeared in these sites + or app are not sponsored, endorsed, or otherwise approved by PHROZEN SASU and / or + Jean-Pierre LESUEUR. For these external sites, PHROZEN SASU and / or Jean-Pierre LESUEUR + cannot be held liable for the availability of, or the content located on or through it. + Plus, any losses or damages occurred from using these contents or the internet + generally. + +-------------------------------------------------------------------------------#> + +Add-Type -Assembly System.Windows.Forms + +Add-Type @" + using System; + using System.Runtime.InteropServices; + + public static class User32 + { + [DllImport("User32.dll")] + public static extern bool SetProcessDPIAware(); + } +"@ + +$global:PowerRemoteDesktopVersion = "4.0.0" + +$global:HostSyncHash = [HashTable]::Synchronized(@{ + host = $host + ClipboardText = (Get-Clipboard -Raw) +}) + +$global:EphemeralTrustedServers = @() + +$global:LocalStoragePath = "HKCU:\SOFTWARE\PowerRemoteDesktop_Viewer" +$global:LocalStoragePath_TrustedServers = -join($global:LocalStoragePath, "\TrustedServers") + +enum ClipboardMode { + Disabled = 1 + Receive = 2 + Send = 3 + Both = 4 +} + +enum ProtocolCommand { + Success = 1 + Fail = 2 + RequestSession = 3 + AttachToSession = 4 + BadRequest = 5 + ResourceFound = 6 + ResourceNotFound = 7 + LogonUIAccessDenied = 8 + LogonUIWrongSession = 9 +} + +enum WorkerKind { + Desktop = 1 + Events = 2 +} + +enum BlockSize { + Size32 = 32 + Size64 = 64 + Size96 = 96 + Size128 = 128 + Size256 = 256 + Size512 = 512 +} + +enum PacketSize { + Size1024 = 1024 + Size2048 = 2048 + Size4096 = 4096 + Size8192 = 8192 + Size9216 = 9216 + Size12288 = 12288 + Size16384 = 16384 +} + +function Write-Banner +{ + <# + .SYNOPSIS + Output cool information about current PowerShell module to terminal. + #> + + Write-Host "" + Write-Host "Power Remote Desktop - Version " -NoNewLine + Write-Host $global:PowerRemoteDesktopVersion -ForegroundColor Cyan + Write-Host "Jean-Pierre LESUEUR (" -NoNewLine + Write-Host "@DarkCoderSc" -NoNewLine -ForegroundColor Green + Write-Host ") " -NoNewLine + Write-Host "#" -NoNewLine -ForegroundColor Blue + Write-Host "#" -NoNewLine -ForegroundColor White + Write-Host "#" -ForegroundColor Red + Write-Host "https://" -NoNewLine -ForegroundColor Green + Write-Host "www.github.com/darkcodersc" + Write-Host "https://" -NoNewLine -ForegroundColor Green + Write-Host "www.phrozen.io" + Write-Host "" + Write-Host "License: Apache License (Version 2.0, January 2004)" + Write-Host "https://" -NoNewLine -ForegroundColor Green + Write-Host "www.apache.org/licenses/" + Write-Host "" +} + +function Get-BooleanAnswer +{ + <# + .SYNOPSIS + As user to make a boolean choice. Return True if Y and False if N. + #> + while ($true) + { + $choice = Read-Host "[Y] Yes [N] No (Default is ""N"")" + if (-not $choice) + { + $choice = "N" + } + + switch ($choice) + { + "Y" + { + return $true + } + + "N" + { + return $false + } + + default + { + Write-Host "Invalid Answer, available options are ""Y , N""" -ForegroundColor Red + } + } + } +} + +function New-RegistryStorage +{ + <# + .SYNOPSIS + Create required registry keys for storing persistent data between viewer + sessions. + + .DESCRIPTION + Users doesn't share this storage. If you really wish to, replace HKCU by HKLM (Requires Admin Privilege) + #> + + try + { + if (-not (Test-Path -Path $global:LocalStoragePath)) + { + Write-Verbose "Create local storage root at ""${global:LocalStoragePath}""..." + + New-Item -Path $global:LocalStoragePath + } + + if (-not (Test-Path -Path $global:LocalStoragePath_TrustedServers)) + { + Write-Verbose "Create local storage child: ""${global:LocalStoragePath}""..." + + New-Item -Path $global:LocalStoragePath_TrustedServers + } + } + catch + { + Write-Verbose "Could not write server fingerprint to local storage with error: ""$($_)""" + } +} + +function Write-ServerFingerprintToLocalStorage +{ + <# + .SYNOPSIS + Write a trusted server certificate fingerprint to our local storage. + + .PARAMETER Fingerprint + Type: String + Default: None + Description: Fingerprint to store in local storage. + #> + param ( + [Parameter(Mandatory=$True)] + [string] $Fingerprint + ) + + New-RegistryStorage + + # Value is stored as a JSON Object to be easily upgraded and extended in future. + $value = New-Object -TypeName PSCustomObject -Property @{ + FirstSeen = (Get-Date).ToString() + } + + New-ItemProperty -Path $global:LocalStoragePath_TrustedServers -Name $Fingerprint -PropertyType "String" -Value ($value | ConvertTo-Json -Compress) -ErrorAction Ignore +} + +function Remove-TrustedServer +{ + <# + .SYNOPSIS + Remove trusted server from local storage. + + .PARAMETER Fingerprint + Type: String + Default: None + Description: Fingerprint to remove from local storage. + #> + param ( + [Parameter(Mandatory=$True)] + [string] $Fingerprint + ) + + if (-not (Test-ServerFingerprintFromLocalStorage -Fingerprint $Fingerprint)) + { + throw "Could not find fingerprint on trusted server list." + } + + Write-Host "You are about to permanently delete trusted server -> """ -NoNewline + Write-Host $Fingerprint -NoNewLine -ForegroundColor Green + Write-Host """" + + Write-Host "Are you sure ?" + + if (Get-BooleanAnswer) + { + Remove-ItemProperty -Path $global:LocalStoragePath_TrustedServers -Name $Fingerprint + + Write-Host "Server successfully untrusted." + } +} + +function Get-TrustedServers +{ + <# + .SYNOPSIS + Return a list of trusted servers fingerprints from local storage. + #> + + $list = @() + + Get-Item -Path $global:LocalStoragePath_TrustedServers -ErrorAction Ignore | Select-Object -ExpandProperty Property | ForEach-Object { + try + { + $list += New-Object -TypeName PSCustomObject -Property @{ + Fingerprint = $_ + Detail = (Get-ItemPropertyValue -Path $global:LocalStoragePath_TrustedServers -Name $_) | ConvertFrom-Json + } + } + catch + { } + } + + return $list +} + +function Clear-TrustedServers +{ + <# + .SYNOPSIS + Remove all trusted servers from local storage. + #> + + $trustedServers = Get-TrustedServers + if (@($trustedServers).Length -eq 0) + { + throw "No trusted servers so far." + } + + Write-Host "You are about to permanently delete $(@(trustedServers).Length) trusted servers." + Write-Host "Are you sure ?" + + if (Get-BooleanAnswer) + { + Remove-Item -Path $global:LocalStoragePath_TrustedServers -Force -Verbose + + Write-Host "Servers successfully untrusted." + } +} + +function Test-ServerFingerprintFromLocalStorage +{ + <# + .SYNOPSIS + Check if a server certificate fingerprint was saved to local storage. + + .PARAMETER Fingerprint + Type: String + Default: None + Description: Fingerprint to check in local storage. + #> + param ( + [Parameter(Mandatory=$True)] + [string] $Fingerprint + ) + + return (Get-ItemProperty -Path $global:LocalStoragePath_TrustedServers -Name $Fingerprint -ErrorAction Ignore) +} + +function Get-SHA512FromString +{ + <# + .SYNOPSIS + Return the SHA512 value from string. + + .PARAMETER String + Type: String + Default : None + Description: A String to hash. + + .EXAMPLE + Get-SHA512FromString -String "Hello, World" + #> + param ( + [Parameter(Mandatory=$True)] + [string] $String + ) + + $buffer = [IO.MemoryStream]::new([byte[]][char[]]$String) + + return (Get-FileHash -InputStream $buffer -Algorithm SHA512).Hash +} + +function Resolve-AuthenticationChallenge +{ + <# + .SYNOPSIS + Algorithm to solve the server challenge during password authentication. + + .DESCRIPTION + Server needs to resolve the challenge and keep the solution in memory before sending + the candidate to remote peer. + + .PARAMETER Password + Type: SecureString + Default: None + Description: Secure String object containing the password for resolving challenge. + + .PARAMETER Candidate + Type: String + Default: None + Description: + Random string used to solve the challenge. This string is public and is set across network by server. + Each time a new connection is requested to server, a new candidate is generated. + + .EXAMPLE + Resolve-AuthenticationChallenge -Password "s3cr3t!" -Candidate "rKcjdh154@]=Ldc" + #> + param ( + [Parameter(Mandatory=$True)] + [SecureString] $SecurePassword, + + [Parameter(Mandatory=$True)] + [string] $Candidate + ) + + $BSTR = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword) + try + { + $solution = -join($Candidate, ":", [Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR)) + + for ([int] $i = 0; $i -le 1000; $i++) + { + $solution = Get-SHA512FromString -String $solution + } + + return $solution + } + finally + { + [Runtime.InteropServices.Marshal]::FreeBSTR($BSTR) + } +} + +class ClientIO { + [string] $RemoteAddress + [int] $RemotePort + [bool] $UseTLSv1_3 + + [System.Net.Sockets.TcpClient] $Client = $null + [System.Net.Security.SslStream] $SSLStream = $null + [System.IO.StreamWriter] $Writer = $null + [System.IO.StreamReader] $Reader = $null + [System.IO.BinaryReader] $BinaryReader = $null + + ClientIO( + [string] $RemoteAddress = "127.0.0.1", + [int] $RemotePort = 2801, + [bool] $UseTLSv1_3 = $false + ) { + $this.RemoteAddress = $RemoteAddress + $this.RemotePort = $RemotePort + $this.UseTLSv1_3 = $UseTLSv1_3 + } + + [void]Connect() { + <# + .SYNOPSIS + Open a new connection to remote server. + Create required streams and open a new secure connection with peer. + #> + Write-Verbose "Connect: ""$($this.RemoteAddress):$($this.RemotePort)...""" + + $this.Client = New-Object System.Net.Sockets.TcpClient($this.RemoteAddress, $this.RemotePort) + + Write-Verbose "Connected." + + if ($this.UseTLSv1_3) + { + $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS13 + } + else { + $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS12 + } + + Write-Verbose "Establish an encrypted tunnel using: ${TLSVersion}..." + + $this.SSLStream = New-object System.Net.Security.SslStream( + $this.Client.GetStream(), + $false, + { + param( + $Sendr, + $Certificate, + $Chain, + $Policy + ) + + if ( + (Test-ServerFingerprintFromLocalStorage -Fingerprint $Certificate.Thumbprint) -or + $global:EphemeralTrustedServers -contains $Certificate.Thumbprint + ) + { + Write-Verbose "Fingerprint already known and trusted: ""$($Certificate.Thumbprint)""" + + return $true + } + else + { + Write-Verbose "@Remote Server Certificate:" + Write-Verbose $Certificate + Write-Verbose "---" + + Write-Host "Untrusted Server Certificate Fingerprint: """ -NoNewLine + Write-Host $Certificate.Thumbprint -NoNewline -ForegroundColor Green + Write-Host """" + + while ($true) + { + Write-Host "`r`nDo you want to trust current server ?" + $choice = Read-Host "[A] Always [Y] Yes [N] No [?] Help (Default is ""N"")" + if (-not $choice) + { + $choice = "N" + } + + switch ($choice) + { + "?" + { + Write-Host "" + + Write-Host "[" -NoNewLine + Write-Host "A" -NoNewLine -ForegroundColor Cyan + Write-Host "] Always trust current server (Persistent between PowerShell Instances)" + + Write-Host "[" -NoNewLine + Write-Host "Y" -NoNewLine -ForegroundColor Cyan + Write-Host "] Trust current server during current PowerShell Instance lifetime (Temporary)." + + Write-Host "[" -NoNewLine + Write-Host "N" -NoNewLine -ForegroundColor Cyan + Write-Host "] Don't trust current server. Connection is aborted (Recommeneded if you don't recognize server fingerprint)." + + Write-Host "[" -NoNewLine + Write-Host "?" -NoNewLine -ForegroundColor Cyan + Write-Host "] Current help output." + + Write-Host "" + } + + "A" + { + Write-ServerFingerprintToLocalStorage -Fingerprint $Certificate.Thumbprint + + return $true + } + + "Y" + { + $global:EphemeralTrustedServers += $Certificate.Thumbprint + + return $true + } + + "N" + { + return $false + } + + default + { + Write-Host "Invalid Answer, available options are ""A , Y , N , H""" -ForegroundColor Red + } + } + } + } + } + ) + + $this.SSLStream.AuthenticateAsClient( + "PowerRemoteDesktop", + $null, + $TLSVersion, + $null + ) + + if (-not $this.SSLStream.IsEncrypted) + { + throw "Could not establish a secure communication channel with remote server." + } + + $this.SSLStream.WriteTimeout = 5000 + + $this.Writer = New-Object System.IO.StreamWriter($this.SSLStream) + $this.Writer.AutoFlush = $true + + $this.Reader = New-Object System.IO.StreamReader($this.SSLStream) + + $this.BinaryReader = New-Object System.IO.BinaryReader($this.SSLStream) + + Write-Verbose "Encrypted tunnel opened and ready for use." + } + + [void]Authentify([SecureString] $SecurePassword) { + <# + .SYNOPSIS + Handle authentication process with remote peer. + + .PARAMETER Password + Type: SecureString + Default: None + Description: Secure String object containing the password. + + .EXAMPLE + .Authentify((ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force)) + #> + + Write-Verbose "Authentify with remote server (Challenged-Based Authentication)..." + + $candidate = $this.Reader.ReadLine() + + $challengeSolution = Resolve-AuthenticationChallenge -Candidate $candidate -SecurePassword $SecurePassword + + Write-Verbose "@Challenge:" + Write-Verbose "Candidate: ""${candidate}""" + Write-Verbose "Solution: ""${challengeSolution}""" + Write-Verbose "---" + + $this.Writer.WriteLine($challengeSolution) + + $result = $this.Reader.ReadLine() + if ($result -eq [ProtocolCommand]::Success) + { + Write-Verbose "Solution accepted. Authentication success." + } + else + { + throw "Solution declined. Authentication failed." + } + + } + + [string] RemoteAddress() { + return $this.Client.Client.RemoteEndPoint.Address + } + + [int] RemotePort() { + return $this.Client.Client.RemoteEndPoint.Port + } + + [string] LocalAddress() { + return $this.Client.Client.LocalEndPoint.Address + } + + [int] LocalPort() { + return $this.Client.Client.LocalEndPoint.Port + } + + [string] ReadLine([int] $Timeout) + { + <# + .SYNOPSIS + Read string message from remote peer with timeout support. + + .PARAMETER Timeout + Type: Integer + Description: Maximum period of time to wait for incomming data. + #> + $defautTimeout = $this.SSLStream.ReadTimeout + try + { + $this.SSLStream.ReadTimeout = $Timeout + + return $this.Reader.ReadLine() + } + finally + { + $this.SSLStream.ReadTimeout = $defautTimeout + } + } + + [string] ReadLine() + { + <# + .SYNOPSIS + Shortcut to Reader ReadLine method. No timeout support. + #> + return $this.Reader.ReadLine() + } + + [void] WriteJson([PSCustomObject] $Object) + { + <# + .SYNOPSIS + Transform a PowerShell Object as a JSON Representation then send to remote + peer. + + .PARAMETER Object + Type: PSCustomObject + Description: Object to be serialized in JSON. + #> + + $this.Writer.WriteLine(($Object | ConvertTo-Json -Compress)) + } + + [void] WriteLine([string] $Value) + { + $this.Writer.WriteLine($Value) + } + + [PSCustomObject] ReadJson([int] $Timeout) + { + <# + .SYNOPSIS + Read json string from remote peer and attempt to deserialize as a PowerShell Object. + + .PARAMETER Timeout + Type: Integer + Description: Maximum period of time to wait for incomming data. + #> + return ($this.ReadLine($Timeout) | ConvertFrom-Json) + } + + [PSCustomObject] ReadJson() + { + <# + .SYNOPSIS + Alternative to ReadJson without timeout support. + #> + return ($this.ReadLine() | ConvertFrom-Json) + } + + [void]Close() { + <# + .SYNOPSIS + Release Streams and Connections. + #> + if ($this.Writer) + { + $this.Writer.Close() + } + + if ($this.Reader) + { + $this.Reader.Close() + } + + if ($this.BinaryReader) + { + $this.BinaryReader.Close() + } + + if ($this.SSLStream) + { + $this.SSLStream.Close() + } + + if ($this.Client) + { + $this.Client.Close() + } + } +} + +class ViewerConfiguration +{ + [bool] $RequireResize = $false + [int] $RemoteDesktopWidth = 0 + [int] $RemoteDesktopHeight = 0 + [int] $VirtualDesktopWidth = 0 + [int] $VirtualDesktopHeight = 0 + [int] $ScreenX_Delta = 0 + [int] $ScreenY_Delta = 0 + [float] $ScreenX_Ratio = 1 + [float] $ScreenY_Ratio = 1 +} + +class ViewerSession +{ + [PSCustomObject] $ServerInformation = $null + [ViewerConfiguration] $ViewerConfiguration = $null + + [string] $ServerAddress = "127.0.0.1" + [string] $ServerPort = 2801 + [SecureString] $SecurePassword = $null + [bool] $UseTLSv1_3 = $false + [int] $ImageCompressionQuality = 100 + [int] $ResizeRatio = 0 + [PacketSize] $PacketSize = [PacketSize]::Size9216 + [BlockSize] $BlockSize = [BlockSize]::Size64 + [bool] $LogonUI = $false + + [ClientIO] $ClientDesktop = $null + [ClientIO] $ClientEvents = $null + + ViewerSession( + [string] $ServerAddress, + [int] $ServerPort, + [SecureString] $SecurePassword + ) + { + # Or: System.Management.Automation.Runspaces.MaxPort (High(Word)) + if ($ServerPort -lt 0 -and $ServerPort -gt 65535) + { + throw "Invalid TCP Port (0-65535)" + } + + $this.ServerAddress = $ServerAddress + $this.ServerPort = $ServerPort + $this.SecurePassword = $SecurePassword + } + + [void] OpenSession() { + <# + .SYNOPSIS + Request a new session with remote server. + #> + + Write-Verbose "Request new session with remote server: ""$($this.ServerAddress):$($this.ServerPort)""..." + + if ($this.ServerInformation) + { + throw "A session already exists." + } + + Write-Verbose "Establish first contact with remote server..." + + $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.UseTLSv1_3) + try + { + $client.Connect() + + $client.Authentify($this.SecurePassword) + + Write-Verbose "Request session..." + + $client.WriteLine(([ProtocolCommand]::RequestSession)) + + $this.ServerInformation = $client.ReadJson() + + Write-Verbose "@ServerInformation:" + Write-Verbose $this.ServerInformation + Write-Verbose "---" + + if ( + (-not ($this.ServerInformation.PSobject.Properties.name -contains "SessionId")) -or + (-not ($this.ServerInformation.PSobject.Properties.name -contains "Version")) -or + (-not ($this.ServerInformation.PSobject.Properties.name -contains "ViewOnly")) -or + + (-not ($this.ServerInformation.PSobject.Properties.name -contains "MachineName")) -or + (-not ($this.ServerInformation.PSobject.Properties.name -contains "Username")) -or + (-not ($this.ServerInformation.PSobject.Properties.name -contains "WindowsVersion")) -or + (-not ($this.ServerInformation.PSobject.Properties.name -contains "Screens")) + ) + { + throw "Invalid server information object." + } + + Write-Verbose "Server informations acknowledged, prepare and send our expectation..." + + if ($this.ServerInformation.Version -ne $global:PowerRemoteDesktopVersion) + { + throw "Server and Viewer version mismatch.`r`n` + Local: ""${global:PowerRemoteDesktopVersion}""`r`n` + Remote: ""$($this.ServerInformation.Version)""`r`n` + You cannot use two different version between Viewer and Server." + } + + if ($this.ServerInformation.ViewOnly) + { + Write-Host "You are accessing a read-only desktop." -ForegroundColor Cyan + } + + # Define which screen we want to capture + $selectedScreen = $null + + if ($this.ServerInformation.Screens.Length -gt 1) + { + Write-Verbose "Remote server have $($this.ServerInformation.Screens.Length) screens." + + Write-Host "Remote server have " -NoNewLine + Write-Host $($this.ServerInformation.Screens.Length) -NoNewLine -ForegroundColor Green + Write-Host " different screens:`r`n" + + foreach ($screen in $this.ServerInformation.Screens) + { + Write-Host $screen.Id -NoNewLine -ForegroundColor Cyan + Write-Host " - $($screen.Name)" -NoNewLine + + if ($screen.Primary) + { + Write-Host " (" -NoNewLine + Write-Host "Primary" -NoNewLine -ForegroundColor Cyan + Write-Host ")" -NoNewLine + } + + Write-Host "" + } + + while ($true) + { + $choice = Read-Host "`r`nPlease choose which screen index to capture (Default: Primary)" + + if (-not $choice) + { + # Select-Object -First 1 should also grab the Primary Screen (Since it is ordered). + $selectedScreen = $this.ServerInformation.Screens | Where-Object -FilterScript { $_.Primary -eq $true } + } + else + { + if (-not $choice -is [int]) { + Write-Host "You must enter a valid index (integer), starting at 1." + + continue + } + + $selectedScreen = $this.ServerInformation.Screens | Where-Object -FilterScript { $_.Id -eq $choice } + + if (-not $selectedScreen) + { + Write-Host "Invalid choice, please choose an existing screen index." -ForegroundColor Red + } + } + + if ($selectedScreen) + { + break + } + } + } + else + { + $selectedScreen = $this.ServerInformation.Screens | Select-Object -First 1 + } + + # Define our Virtual Desktop Form constraints + $localScreenWidth = Get-LocalScreenWidth + $localScreenHeight = (Get-LocalScreenHeight) - (Get-WindowCaptionHeight) + + $this.ViewerConfiguration = [ViewerConfiguration]::New() + + $this.ViewerConfiguration.RemoteDesktopWidth = $selectedScreen.Width + $this.ViewerConfiguration.RemoteDesktopHeight = $selectedScreen.Height + + # If remote screen is bigger than local screen, we will resize remote screen to fit 90% of local screen. + # Supports screen orientation (Horizontal / Vertical) + if ($localScreenWidth -le $selectedScreen.Width -or $localScreenHeight -le $selectedScreen.Height) + { + $adjustRatio = 90 + + $adjustVertically = $localScreenWidth -gt $localScreenHeight + + if ($adjustVertically) + { + $this.ViewerConfiguration.VirtualDesktopWidth = [math]::Round(($localScreenWidth * $adjustRatio) / 100) + + $remoteResizedRatio = [math]::Round(($this.ViewerConfiguration.VirtualDesktopWidth * 100) / $selectedScreen.Width) + + $this.ViewerConfiguration.VirtualDesktopHeight = [math]::Round(($selectedScreen.Height * $remoteResizedRatio) / 100) + } + else + { + $this.ViewerConfiguration.VirtualDesktopHeight = [math]::Round(($localScreenHeight * $adjustRatio) / 100) + + $remoteResizedRatio = [math]::Round(($this.ViewerConfiguration.VirtualDesktopHeight * 100) / $selectedScreen.Height) + + $this.ViewerConfiguration.VirtualDesktopWidth = [math]::Round(($selectedScreen.Width * $remoteResizedRatio) / 100) + } + } + else + { + $this.ViewerConfiguration.VirtualDesktopWidth = $selectedScreen.Width + $this.ViewerConfiguration.VirtualDesktopHeight = $selectedScreen.Height + } + + # If remote desktop resize is forced, we apply defined ratio to current configuration + if ($this.ResizeRatio -ge 30 -and $this.ResizeRatio -le 99) + { + $this.ViewerConfiguration.VirtualDesktopWidth = ($selectedScreen.Width * $this.ResizeRatio) / 100 + $this.ViewerConfiguration.VirtualDesktopHeight = ($selectedScreen.Height * $this.ResizeRatio) / 100 + } + + $this.ViewerConfiguration.RequireResize = $this.ViewerConfiguration.VirtualDesktopWidth -ne $selectedScreen.Width -or + $this.ViewerConfiguration.VirtualDesktopHeight -ne $selectedScreen.Height + + $this.ViewerConfiguration.ScreenX_Delta = $selectedScreen.X + $this.ViewerConfiguration.ScreenY_Delta = $selectedScreen.Y + + if ($this.ViewerConfiguration.RequireResize) + { + $this.ViewerConfiguration.ScreenX_Ratio = $selectedScreen.Width / $this.ViewerConfiguration.VirtualDesktopWidth + $this.ViewerConfiguration.ScreenY_Ratio = $selectedScreen.Height / $this.ViewerConfiguration.VirtualDesktopHeight + } + + $viewerExpectation = New-Object PSCustomObject -Property @{ + ScreenName = $selectedScreen.Name + ImageCompressionQuality = $this.ImageCompressionQuality + PacketSize = $this.PacketSize + BlockSize = $this.BlockSize + LogonUI = $this.LogonUI + } + + Write-Verbose "@ViewerExpectation:" + Write-Verbose $viewerExpectation + Write-Verbose "---" + + $client.WriteJson($viewerExpectation) + + switch ([ProtocolCommand] $client.ReadLine(5 * 1000)) + { + ([ProtocolCommand]::Success) + { + break + } + + ([ProtocolCommand]::LogonUIAccessDenied) + { + throw "Could not access LogonUI / Winlogon desktop.`r`n" + + "To access LogonUI desktop, you must have ""NT AUTHORITY/System"" privilege in current active session." + + break + } + + ([ProtocolCommand]::LogonUIWrongSession) + { + throw "Could not access LogonUI / Winlogon desktop.`r`n" + "To access LogonUI desktop, server process must be running under active Windows Session." + + break + } + + default + { + throw "Remote server did not acknoledged our expectation in time." + } + } + } + catch + { + $this.CloseSession() + + throw "Could not open a new session with error: ""$($_)""" + } + finally + { + if ($client) + { + $client.Close() + } + } + } + + [ClientIO] ConnectWorker([WorkerKind] $WorkerKind) + { + Write-Verbose "Connect new worker: ""$WorkerKind""..." + + $this.CheckSession() + + $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.UseTLSv1_3) + try + { + $client.Connect() + + $client.Authentify($this.SecurePassword) + + $client.WriteLine(([ProtocolCommand]::AttachToSession)) + + Write-Verbose "Attach worker to remote session ""$($this.ServerInformation.SessionId)""" + + $client.WriteLine($this.ServerInformation.SessionId) + + switch ([ProtocolCommand] $client.ReadLine(5 * 1000)) + { + ([ProtocolCommand]::ResourceFound) + { + Write-Verbose "Worker successfully attached to session, define which kind of worker we are..." + + $client.WriteLine($WorkerKind) + + Write-Verbose "Worker ready." + + break + } + + ([ProtocolCommand]::ResourceNotFound) + { + throw "Server could not locate session." + } + + default + { + throw "Unexpected answer from remote server for session attach." + } + } + + return $client + } + catch + { + if ($client) + { + $client.Close() + } + + throw "Could not connect worker with error: $($_)" + } + } + + [void] ConnectDesktopWorker() + { + Write-Verbose "Create new desktop streaming worker..." + + $this.ClientDesktop = $this.ConnectWorker([WorkerKind]::Desktop) + } + + [void] ConnectEventsWorker() + { + Write-Verbose "Create new event event (in/out) worker..." + + $this.ClientEvents = $this.ConnectWorker([WorkerKind]::Events) + } + + [bool] HasSession() + { + return $this.ServerInformation -and $this.ViewerConfiguration + } + + [void] CheckSession() + { + if (-not $this.HasSession) + { + throw "Session is missing." + } + } + + [void] CloseSession() { + <# + .SYNOPSIS + Close an existing session with remote server. + Terminate active connections and reset session informations. + #> + + Write-Verbose "Close existing session..." + + if ($this.ClientDesktop) + { + $this.ClientDesktop.Close() + } + + if ($this.ClientEvents) + { + $this.ClientEvents.Close() + } + + $this.ClientDesktop = $null + $this.ClientEvents = $null + + $this.ServerInformation = $null + $this.ViewerConfiguration = $null + + Write-Verbose "Session closed." + } + +} + +$global:VirtualDesktopUpdaterScriptBlock = { + try + { + $packetSize = [int]$Param.packetSize + + # SizeOf(DWORD) * 3 (SizeOf(Desktop) + SizeOf(Left) + SizeOf(Top)) + $struct = New-Object -TypeName byte[] -ArgumentList (([Runtime.InteropServices.Marshal]::SizeOf([System.Type][UInt32])) * 3) + + $stream = New-Object System.IO.MemoryStream + + $scene = $null + $sceneGraphics = $null + + $destPoint = [System.Drawing.Point]::New(0, 0) + + $scene = [System.Drawing.Bitmap]::New( + $Param.ViewerConfiguration.RemoteDesktopWidth, + $Param.ViewerConfiguration.RemoteDesktopHeight + ) + + $sceneGraphics = [System.Drawing.Graphics]::FromImage($scene) + $sceneGraphics.CompositingMode = [System.Drawing.Drawing2D.CompositingMode]::SourceCopy + + $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Image = $scene # Assign our scene + + # Wait until the virtual desktop form is shown to user desktop. + while (-not $Param.VirtualDesktopSyncHash.VirtualDesktop.Form.Visible) + { + Start-Sleep -Milliseconds 100 + } + + # Tiny hack to correctly bring to front window, this is the most effective technique so far. + $Param.VirtualDesktopSyncHash.VirtualDesktop.Form.TopMost = $true + $Param.VirtualDesktopSyncHash.VirtualDesktop.Form.TopMost = $false + + while ($true) + { + try + { + $null = $Param.Client.SSLStream.Read($struct, 0, $struct.Length) + + $totalBufferSize = [System.Runtime.InteropServices.Marshal]::ReadInt32($struct, 0x0) + $destPoint.X = [System.Runtime.InteropServices.Marshal]::ReadInt32($struct, 0x4) + $destPoint.Y = [System.Runtime.InteropServices.Marshal]::ReadInt32($struct, 0x8) + + $stream.SetLength($totalBufferSize) + + $stream.Position = 0 + do + { + $bufferSize = $stream.Length - $stream.Position + if ($bufferSize -gt $packetSize) + { + $bufferSize = $packetSize + } + + $null = $stream.Write($Param.Client.BinaryReader.ReadBytes($bufferSize), 0, $bufferSize) + } until ($stream.Position -eq $stream.Length) + + if ($stream.Length -eq 0) + { + continue + } + + # Next Iterations + $sceneGraphics.DrawImage( + [System.Drawing.Image]::FromStream($stream), + $destPoint + ) + + $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Invalidate() + } + catch + { + break + } + } + } + finally + { + if ($scene) + { + $scene.Dispose() + } + + if ($sceneGraphics) + { + $sceneGraphics.Dispose() + } + + if ($stream) + { + $stream.Close() + } + + $Param.VirtualDesktopSyncHash.VirtualDesktop.Form.Close() + } +} + +$global:IngressEventScriptBlock = { + + enum CursorType { + IDC_APPSTARTING + IDC_ARROW + IDC_CROSS + IDC_HAND + IDC_HELP + IDC_IBEAM + IDC_ICON + IDC_NO + IDC_SIZE + IDC_SIZEALL + IDC_SIZENESW + IDC_SIZENS + IDC_SIZENWSE + IDC_SIZEWE + IDC_UPARROW + IDC_WAIT + } + + enum InputEvent { + KeepAlive = 0x1 + MouseCursorUpdated = 0x2 + ClipboardUpdated = 0x3 + DesktopActive = 0x4 + DesktopInactive = 0x5 + } + + enum ClipboardMode { + Disabled = 1 + Receive = 2 + Send = 3 + Both = 4 + } + + while ($true) + { + try + { + $jsonEvent = $Param.Client.Reader.ReadLine() + } + catch + { break } + + try + { + $aEvent = $jsonEvent | ConvertFrom-Json + } + catch + { continue } + + if (-not ($aEvent.PSobject.Properties.name -match "Id")) + { continue } + + switch ([InputEvent] $aEvent.Id) + { + # Remote Global Mouse Cursor State Changed (Icon) + ([InputEvent]::MouseCursorUpdated) + { + if (-not ($aEvent.PSobject.Properties.name -match "Cursor")) + { continue } + + $cursor = [System.Windows.Forms.Cursors]::Arrow + + switch ([CursorType] $aEvent.Cursor) + { + ([CursorType]::IDC_APPSTARTING) { $cursor = [System.Windows.Forms.Cursors]::AppStarting } + ([CursorType]::IDC_CROSS) { $cursor = [System.Windows.Forms.Cursors]::Cross } + ([CursorType]::IDC_HAND) { $cursor = [System.Windows.Forms.Cursors]::Hand } + ([CursorType]::IDC_HELP) { $cursor = [System.Windows.Forms.Cursors]::Help } + ([CursorType]::IDC_IBEAM) { $cursor = [System.Windows.Forms.Cursors]::IBeam } + ([CursorType]::IDC_NO) { $cursor = [System.Windows.Forms.Cursors]::No } + ([CursorType]::IDC_SIZENESW) { $cursor = [System.Windows.Forms.Cursors]::SizeNESW } + ([CursorType]::IDC_SIZENS) { $cursor = [System.Windows.Forms.Cursors]::SizeNS } + ([CursorType]::IDC_SIZENWSE) { $cursor = [System.Windows.Forms.Cursors]::SizeNWSE } + ([CursorType]::IDC_SIZEWE) { $cursor = [System.Windows.Forms.Cursors]::SizeWE } + ([CursorType]::IDC_UPARROW) { $cursor = [System.Windows.Forms.Cursors]::UpArrow } + ([CursorType]::IDC_WAIT) { $cursor = [System.Windows.Forms.Cursors]::WaitCursor } + + {( $_ -eq ([CursorType]::IDC_SIZE) -or $_ -eq ([CursorType]::IDC_SIZEALL) )} + { + $cursor = [System.Windows.Forms.Cursors]::SizeAll + } + } + + try + { + $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Cursor = $cursor + } + catch + {} + + break + } + + ([InputEvent]::ClipboardUpdated) + { + if ($Param.Clipboard -eq ([ClipboardMode]::Disabled) -or $Param.Clipboard -eq ([ClipboardMode]::Send)) + { continue } + + if (-not ($aEvent.PSobject.Properties.name -match "Text")) + { continue } + + $HostSyncHash.ClipboardText = $aEvent.Text + + Set-Clipboard -Value $aEvent.Text + + break + } + + ([InputEvent]::DesktopActive) + { + break + } + + ([InputEvent]::DesktopInactive) + { + break + } + } + } +} + +$global:EgressEventScriptBlock = { + + enum OutputEvent { + # 0x1 0x2 0x3 are at another place (GUI Thread) + KeepAlive = 0x4 + ClipboardUpdated = 0x5 + } + + enum ClipboardMode { + Disabled = 1 + Receive = 2 + Send = 3 + Both = 4 + } + + function Send-Event + { + <# + .SYNOPSIS + Send an event to remote peer. + + .PARAMETER AEvent + Define what kind of event to send. + + .PARAMETER Data + An optional object containing additional information about the event. + #> + param ( + [Parameter(Mandatory=$True)] + [OutputEvent] $AEvent, + + [PSCustomObject] $Data = $null + ) + + try + { + if (-not $Data) + { + $Data = New-Object -TypeName PSCustomObject -Property @{ + Id = $AEvent + } + } + else + { + $Data | Add-Member -MemberType NoteProperty -Name "Id" -Value $AEvent + } + + $Param.OutputEventSyncHash.Writer.WriteLine(($Data | ConvertTo-Json -Compress)) + + return $true + } + catch + { + return $false + } + } + + $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + + while ($true) + { + # Events that occurs every seconds needs to be placed bellow. + # If no event has occured during this second we send a Keep-Alive signal to + # remote peer and detect a potential socket disconnection. + if ($stopWatch.ElapsedMilliseconds -ge 1000) + { + try + { + $eventTriggered = $false + + if ($Param.Clipboard -eq ([ClipboardMode]::Both) -or $Param.Clipboard -eq ([ClipboardMode]::Send)) + { + # IDEA: Check for existing clipboard change event or implement a custom clipboard + # change detector using "WM_CLIPBOARDUPDATE" for example (WITHOUT INLINE C#) + # It is not very important but it would avoid calling "Get-Clipboard" every seconds. + $currentClipboard = (Get-Clipboard -Raw) + + if ($currentClipboard -and $currentClipboard -cne $HostSyncHash.ClipboardText) + { + $data = New-Object -TypeName PSCustomObject -Property @{ + Text = $currentClipboard + } + + if (-not (Send-Event -AEvent ([OutputEvent]::ClipboardUpdated) -Data $data)) + { break } + + $HostSyncHash.ClipboardText = $currentClipboard + + $eventTriggered = $true + } + } + + # Send a Keep-Alive if during this second iteration nothing happened. + if (-not $eventTriggered) + { + if (-not (Send-Event -AEvent ([OutputEvent]::KeepAlive))) + { break } + } + } + finally + { + $stopWatch.Restart() + } + } + } +} + +function Get-WindowCaptionHeight +{ + $form = New-Object System.Windows.Forms.Form + try { + $screenRect = $form.RectangleToScreen($form.ClientRectangle) + + return $screenRect.Top - $virtualDesktopSyncHash.VirtualDesktop.Form.Top + } + finally + { + if ($form) + { + $form.Dispose() + } + } +} + +function Get-LocalScreenWidth +{ + return [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Width +} + +function Get-LocalScreenHeight +{ + return [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Height +} + +function New-VirtualDesktopForm +{ + <# + .SYNOPSIS + Create new WinForms Components to handle Virtual Desktop. + + .DESCRIPTION + This function first create a new Windows Form then create a new child component (PaintBox) + to display remote desktop frames. + + It returns a PowerShell object containing both Form and PaintBox. + + .PARAMETER Width + Type: Integer + Default: 1200 + Description: The pre-defined width of new form + + .PARAMETER Height + Type: Integer + Default: 800 + Description: The pre-defined height of new form + + .PARAMETER Caption + Type: String + Default: PowerRemoteDesktop Viewer + Description: The pre-defined caption of new form. + + .EXAMPLE + New-VirtualDesktopForm -Caption "New Desktop Form" -Width 1200 -Height 800 + #> + param ( + [int] $Width = 1200, + [int] $Height = 800, + [string] $Caption = "PowerRemoteDesktop Viewer" + ) + + $form = New-Object System.Windows.Forms.Form + + $form.Width = $Width + $form.Height = $Height + $form.BackColor = [System.Drawing.Color]::Black + $form.Text = $Caption + $form.KeyPreview = $true # Necessary to capture keystrokes. + $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedSingle + $form.MaximizeBox = $false + + $pictureBox = New-Object System.Windows.Forms.PictureBox + $pictureBox.Dock = [System.Windows.Forms.DockStyle]::Fill + $pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::StretchImage + + $form.Controls.Add($pictureBox) + + return New-Object PSCustomObject -Property @{ + Form = $form + Picture = $pictureBox + } +} + +function New-RunSpace +{ + <# + .SYNOPSIS + Create a new PowerShell Runspace. + + .DESCRIPTION + Notice: the $host variable is used for debugging purpose to write on caller PowerShell + Terminal. + + .PARAMETER ScriptBlock + Type: ScriptBlock + Default: None + Description: Instructions to execute in new runspace. + + .PARAMETER Param + Type: PSCustomObject + Default: None + Description: Object to attach in runspace context. + + .EXAMPLE + New-RunSpace -Client $newClient -ScriptBlock { Start-Sleep -Seconds 10 } + #> + + param( + [Parameter(Mandatory=$True)] + [ScriptBlock] $ScriptBlock, + + [PSCustomObject] $Param = $null + ) + + $runspace = [RunspaceFactory]::CreateRunspace() + $runspace.ThreadOptions = "ReuseThread" + $runspace.ApartmentState = "STA" + $runspace.Open() + + if ($Param) + { + $runspace.SessionStateProxy.SetVariable("Param", $Param) + } + + $runspace.SessionStateProxy.SetVariable("HostSyncHash", $global:HostSyncHash) + + $powershell = [PowerShell]::Create().AddScript($ScriptBlock) + + $powershell.Runspace = $runspace + + $asyncResult = $powershell.BeginInvoke() + + return New-Object PSCustomObject -Property @{ + Runspace = $runspace + PowerShell = $powershell + AsyncResult = $asyncResult + } +} + +function Invoke-RemoteDesktopViewer +{ + <# + .SYNOPSIS + Open a new remote desktop session with a remote server. + + .DESCRIPTION + Notice: Prefer using SecurePassword over plain-text password even if a plain-text password is getting converted to SecureString anyway. + + .PARAMETER ServerAddress + Type: String + Default: 127.0.0.1 + Description: Remote server host/address. + + .PARAMETER ServerPort + Type: Integer + Default: 2801 (0 - 65535) + Description: Remote server port. + + .PARAMETER SecurePassword + Type: SecureString + Default: None + Description: SecureString object containing password used to authenticate with remote server (Recommended) + + .PARAMETER Password + Type: String + Default: None + Description: Plain-Text Password used to authenticate with remote server (Not recommended, use SecurePassword instead) + + .PARAMETER UseTLSv1_3 + Type: Switch + Default: False + Description: If present, TLS v1.3 will be used instead of TLS v1.2 (Recommended if applicable to both systems) + + .PARAMETER DisableVerbosity + Type: Boolean + Default: False + Description: If present, program wont show verbosity messages. + + .PARAMETER Clipboard + Type: Enum + Default: Both + Description: + Define clipboard synchronization mode (Both, Disabled, Send, Receive) see bellow for more detail. + + * Disabled -> Clipboard synchronization is disabled in both side + * Receive -> Only incomming clipboard is allowed + * Send -> Only outgoing clipboard is allowed + * Both -> Clipboard synchronization is allowed on both side + + .PARAMETER ImageCompressionQuality + Type: Integer (0 - 100) + Default: 75 + Description: JPEG Compression level from 0 to 100. 0 = Lowest quality, 100 = Highest quality. + + .PARAMETER Resize + Type: Switch + Default: None + Description: If present, remote desktop will get resized accordingly with ResizeRatio option. + + .PARAMETER ResizeRatio + Type: Integer (30 - 99) + Default: None + Description: Used with Resize option, define the resize ratio in percentage. + + .PARAMETER AlwaysOnTop + Type: Switch + Default: False + Description: If present, virtual desktop form will be above all other window's. + + .PARAMETER BlockSize + Type: Enum + Values: Size32, Size64, Size96, Size128, Size256, Size512 + Default: Size64 + Description: + (Advanced) Define the screen grid block size. + Choose the block size accordingly to remote screen size / computer constrainsts (CPU / Network) + + Size1024 -> 1024 Bytes (1KiB) + Size2048 -> 2048 Bytes (2KiB) + Size4096 -> 4096 Bytes (4KiB) + Size8192 -> 8192 Bytes (8KiB) + Size9216 -> 9216 Bytes (9KiB) + Size12288 -> 12288 Bytes (12KiB) + Size16384 -> 16384 Bytes (16KiB) + + .PARAMETER PacketSize + Type: Enum + Values: Size1024, Size2048, Size4096, Size8192, Size9216, Size12288, Size16384 + Default: Size9216 + Description: + (Advanced) Define the network packet size for streams. + Choose the packet size accordingly to your network constrainsts. + + Size32 -> 32x32 + Size64 -> 64x64 + Size96 -> 96x96 + Size128 -> 128x128 + Size256 -> 256x256 + Size512 -> 512x512 + + .PARAMETER LogonUI + Type: Switch + Default: None + Description: Request server to open LogonUI / Winlogon desktop insead of default user desktop (Requires SYSTEM privilege in active session). + + .EXAMPLE + Invoke-RemoteDesktopViewer -ServerAddress "192.168.0.10" -ServerPort "2801" -SecurePassword (ConvertTo-SecureString -String "s3cr3t!" -AsPlainText -Force) + Invoke-RemoteDesktopViewer -ServerAddress "192.168.0.10" -ServerPort "2801" -Password "s3cr3t!" + Invoke-RemoteDesktopViewer -ServerAddress "127.0.0.1" -ServerPort "2801" -Password "Just4TestingLocally!" + + #> + param ( + [string] $ServerAddress = "127.0.0.1", + + [ValidateRange(0, 65535)] + [int] $ServerPort = 2801, + + [switch] $UseTLSv1_3, + [SecureString] $SecurePassword, + [String] $Password, + [switch] $DisableVerbosity, + [ClipboardMode] $Clipboard = [ClipboardMode]::Both, + + [ValidateRange(0, 100)] + [int] $ImageCompressionQuality = 75, + + [switch] $Resize, + + [ValidateRange(30, 99)] + [int] $ResizeRatio = 90, + + [switch] $AlwaysOnTop, + [PacketSize] $PacketSize = [PacketSize]::Size9216, + [BlockSize] $BlockSize = [BlockSize]::Size64, + [switch] $LogonUI + ) + + [System.Collections.Generic.List[PSCustomObject]]$runspaces = @() + + $oldErrorActionPreference = $ErrorActionPreference + $oldVerbosePreference = $VerbosePreference + try + { + $ErrorActionPreference = "stop" + + if (-not $DisableVerbosity) + { + $VerbosePreference = "continue" + } + else + { + $VerbosePreference = "SilentlyContinue" + } + + Write-Banner + + $null = [User32]::SetProcessDPIAware() + + Write-Verbose "Server address: ""${ServerAddress}:${ServerPort}""" + + if (-not $SecurePassword -and -not $Password) + { + throw "You must specify either a SecurePassword or Password parameter used during server authentication." + } + + if ($Password -and -not $SecurePassword) + { + $SecurePassword = (ConvertTo-SecureString -String $Password -AsPlainText -Force) + + Remove-Variable -Name "Password" -ErrorAction SilentlyContinue + } + + $session = [ViewerSession]::New( + $ServerAddress, + $ServerPort, + $SecurePassword + ) + try + { + $session.UseTLSv1_3 = $UseTLSv1_3 + $session.ImageCompressionQuality = $ImageCompressionQuality + $session.PacketSize = $PacketSize + $session.BlockSize = $BlockSize + $session.LogonUI = $LogonUI + + if ($Resize) + { + $session.ResizeRatio = $ResizeRatio + } + + Write-Host "Start new remote desktop session..." + + $session.OpenSession() + + $session.ConnectDesktopWorker() + + $session.ConnectEventsWorker() + + Write-Host "Session successfully established, start streaming..." + + Write-Verbose "Create WinForms Environment..." + + $virtualDesktop = New-VirtualDesktopForm + $virtualDesktopSyncHash = [HashTable]::Synchronized(@{ + VirtualDesktop = $virtualDesktop + }) + + $virtualDesktop.Form.Text = [string]::Format( + "Power Remote Desktop v{0}: {1}/{2} - {3}", + $global:PowerRemoteDesktopVersion, + $session.ServerInformation.Username, + $session.ServerInformation.MachineName, + $session.ServerInformation.WindowsVersion + ) + + # Size Virtual Desktop Form Window + $virtualDesktop.Form.ClientSize = [System.Drawing.Size]::New( + $session.ViewerConfiguration.VirtualDesktopWidth, + $session.ViewerConfiguration.VirtualDesktopHeight + ) + + # Create a thread-safe hashtable to send events to remote server. + $outputEventSyncHash = [HashTable]::Synchronized(@{ + Writer = $session.ClientEvents.Writer + }) + + # WinForms Events (If enabled, I recommend to disable control when testing on local machine to avoid funny things) + if (-not $session.ServerInformation.ViewOnly) + { + enum OutputEvent { + Keyboard = 0x1 + MouseClickMove = 0x2 + MouseWheel = 0x3 + } + + enum MouseState { + Up = 0x1 + Down = 0x2 + Move = 0x3 + } + + function New-MouseEvent + { + <# + .SYNOPSIS + Generate a new mouse event object to be sent to server. + This event is used to simulate mouse move and clicks. + + .PARAMETER X + Type: Integer + Default: None + Description: The position of mouse in horizontal axis. + + .PARAMETER Y + Type: Integer + Default: None + Description: The position of mouse in vertical axis. + + .PARAMETER Type + Type: Enum + Default: None + Description: The type of mouse event (Example: Move, Click) + + .PARAMETER Button + Type: String + Default: None + Description: The pressed button on mouse (Example: Left, Right, Middle) + + .EXAMPLE + New-MouseEvent -X 10 -Y 35 -Type "Up" -Button "Left" + New-MouseEvent -X 10 -Y 35 -Type "Down" -Button "Left" + New-MouseEvent -X 100 -Y 325 -Type "Move" + #> + param ( + [Parameter(Mandatory=$true)] + [int] $X, + [Parameter(Mandatory=$true)] + [int] $Y, + [Parameter(Mandatory=$true)] + [MouseState] $Type, + + [string] $Button = "None" + ) + + return New-Object PSCustomObject -Property @{ + Id = [OutputEvent]::MouseClickMove + X = $X + Y = $Y + Button = $Button + Type = $Type + } + } + + function New-KeyboardEvent + { + <# + .SYNOPSIS + Generate a new keyboard event object to be sent to server. + This event is used to simulate keyboard strokes. + + .PARAMETER Keys + Type: String + Default: None + Description: Plain text keys to be simulated on remote computer. + + .EXAMPLE + New-KeyboardEvent -Keys "Hello, World" + New-KeyboardEvent -Keys "t" + #> + param ( + [Parameter(Mandatory=$true)] + [string] $Keys + ) + + return New-Object PSCustomObject -Property @{ + Id = [OutputEvent]::Keyboard + Keys = $Keys + } + } + + function Send-VirtualMouse + { + <# + .SYNOPSIS + Transform the virtual mouse (the one in Virtual Desktop Form) coordinates to real remote desktop + screen coordinates (especially when incomming desktop frames are resized) + + When event is generated, it is immediately sent to remote server. + + .PARAMETER X + Type: Integer + Default: None + Description: The position of virtual mouse in horizontal axis. + + .PARAMETER Y + Type: Integer + Default: None + Description: The position of virtual mouse in vertical axis. + + .PARAMETER Type + Type: Integer + Default: None + Description: The type of mouse event (Example: Move, Click) + + .PARAMETER Button + Type: String + Default: None + Description: The pressed button on mouse (Example: Left, Right, Middle) + + .EXAMPLE + Send-VirtualMouse -X 10 -Y 20 -Type "Move" + #> + param ( + [Parameter(Mandatory=$True)] + [int] $X, + [Parameter(Mandatory=$True)] + [int] $Y, + [Parameter(Mandatory=$True)] + [MouseState] $Type, + + [string] $Button = "" + ) + + if ($session.ViewerConfiguration.RequireResize) + { + $X *= $session.ViewerConfiguration.ScreenX_Ratio + $Y *= $session.ViewerConfiguration.ScreenY_Ratio + } + + $X += $session.ViewerConfiguration.ScreenX_Delta + $Y += $session.ViewerConfiguration.ScreenY_Delta + + $aEvent = (New-MouseEvent -X $X -Y $Y -Button $Button -Type $Type) + + try + { + $outputEventSyncHash.Writer.WriteLine(($aEvent | ConvertTo-Json -Compress)) + } + catch + {} + } + + function Send-VirtualKeyboard + { + <# + .SYNOPSIS + Send to remote server key strokes to simulate. + + .PARAMETER KeyChain + Type: String + Default: None + Description: A string representing character(s) to simulate remotely. + + .EXAMPLE + Send-VirtualKeyboard -KeyChain "Hello, World" + Send-VirtualKeyboard -KeyChain "{LEFT}" + #> + param ( + [Parameter(Mandatory=$True)] + [string] $KeyChain + ) + + $aEvent = (New-KeyboardEvent -Keys $KeyChain) + + try + { + $outputEventSyncHash.Writer.WriteLine(($aEvent | ConvertTo-Json -Compress)) + } + catch + {} + } + + $virtualDesktop.Form.Add_KeyPress( + { + if ($_.KeyChar) + { + switch -CaseSensitive ([string]$_.KeyChar) + { + "{" { $result = "{{}" } + "}" { $result = "{}}" } + "+" { $result = "{+}" } + "^" { $result = "{^}" } + "%" { $result = "{%}" } + "~" { $result = "{~}" } + "(" { $result = "{(}" } + ")" { $result = "{)}" } + "[" { $result = "{[}" } + "]" { $result = "{]}" } + + default { $result = $_ } + } + + Send-VirtualKeyboard -KeyChain $result + } + } + ) + + $virtualDesktop.Form.Add_Shown( + { + # Center Virtual Desktop Form + $virtualDesktop.Form.Location = [System.Drawing.Point]::New( + ((Get-LocalScreenWidth) - $virtualDesktop.Form.Width) / 2, + ((Get-LocalScreenHeight) - $virtualDesktop.Form.Height) / 2 + ) + + $virtualDesktop.Form.TopMost = $AlwaysOnTop + } + ) + + $virtualDesktop.Form.Add_KeyDown( + { + $result = "" + + switch ($_.KeyValue) + { + # WIN Key + 91 { $result = "^{ESC}" } + + # F Keys + 112 { $result = "{F1}" } + 113 { $result = "{F2}" } + 114 { $result = "{F3}" } + 115 { $result = "{F4}" } + 116 { $result = "{F5}" } + 117 { $result = "{F6}" } + 118 { $result = "{F7}" } + 119 { $result = "{F8}" } + 120 { $result = "{F9}" } + 121 { $result = "{F10}" } + 122 { $result = "{F11}" } + 123 { $result = "{F12}" } + 124 { $result = "{F13}" } + 125 { $result = "{F14}" } + 126 { $result = "{F15}" } + 127 { $result = "{F16}" } + + # Arrows + 37 { $result = "{LEFT}" } + 38 { $result = "{UP}" } + 39 { $result = "{RIGHT}" } + 40 { $result = "{DOWN}" } + + # Misc + 92 { $result = "{WIN}" } + 27 { $result = "{ESC}"} + 33 { $result = "{PGUP}" } + 34 { $result = "{PGDW}" } + 36 { $result = "{HOME}" } + 46 { $result = "{DELETE}" } + 35 { $result = "{END}" } + + # Add other keys bellow + } + + if ($result) + { + Send-VirtualKeyboard -KeyChain $result + } + } + ) + + $virtualDesktop.Picture.Add_MouseDown( + { + Send-VirtualMouse -X $_.X -Y $_.Y -Button $_.Button -Type ([MouseState]::Down) + } + ) + + $virtualDesktop.Picture.Add_MouseUp( + { + Send-VirtualMouse -X $_.X -Y $_.Y -Button $_.Button -Type ([MouseState]::Up) + } + ) + + $virtualDesktop.Picture.Add_MouseMove( + { + Send-VirtualMouse -X $_.X -Y $_.Y -Button $_.Button -Type ([MouseState]::Move) + } + ) + + $virtualDesktop.Picture.Add_MouseWheel( + { + $aEvent = New-Object PSCustomObject -Property @{ + Id = [OutputEvent]::MouseWheel + Delta = $_.Delta + } + + try + { + $outputEventSyncHash.Writer.WriteLine(($aEvent | ConvertTo-Json -Compress)) + } + catch {} + } + ) + } + + Write-Verbose "Create runspace for desktop streaming..." + + $param = New-Object -TypeName PSCustomObject -Property @{ + Client = $session.ClientDesktop + VirtualDesktopSyncHash = $virtualDesktopSyncHash + ViewerConfiguration = $session.ViewerConfiguration + PacketSize = $session.PacketSize + } + + $newRunspace = (New-RunSpace -ScriptBlock $global:VirtualDesktopUpdaterScriptBlock -Param $param) + $runspaces.Add($newRunspace) + + Write-Verbose "Create runspace for incoming events..." + + $param = New-Object -TypeName PSCustomObject -Property @{ + Client = $session.ClientEvents + VirtualDesktopSyncHash = $virtualDesktopSyncHash + Clipboard = $Clipboard + } + + $newRunspace = (New-RunSpace -ScriptBlock $global:IngressEventScriptBlock -Param $param) + $runspaces.Add($newRunspace) + + Write-Verbose "Create runspace for outgoing events..." + + $param = New-Object -TypeName PSCustomObject -Property @{ + OutputEventSyncHash = $outputEventSyncHash + Clipboard = $Clipboard + } + + $newRunspace = (New-RunSpace -ScriptBlock $global:EgressEventScriptBlock -Param $param) + $runspaces.Add($newRunspace) + + Write-Verbose "Done. Showing Virtual Desktop Form." + + $null = $virtualDesktop.Form.ShowDialog() + } + finally + { + Write-Verbose "Free environement." + + if ($session) + { + $session.CloseSession() + + $session = $null + } + + Write-Verbose "Free runspaces..." + + foreach ($runspace in $runspaces) + { + $null = $runspace.PowerShell.EndInvoke($runspace.AsyncResult) + $runspace.PowerShell.Runspace.Dispose() + $runspace.PowerShell.Dispose() + } + $runspaces.Clear() + + if ($virtualDesktop) + { + $virtualDesktop.Form.Dispose() + } + + Write-Host "Remote desktop session has ended." + } + } + finally + { + $ErrorActionPreference = $oldErrorActionPreference + $VerbosePreference = $oldVerbosePreference + } +} + +try { + Export-ModuleMember -Function Remove-TrustedServer + Export-ModuleMember -Function Clear-TrustedServers + Export-ModuleMember -Function Get-TrustedServers + Export-ModuleMember -Function Invoke-RemoteDesktopViewer +} catch {} \ No newline at end of file diff --git a/deprecated/old project viewer/TestViewer.ps1 b/deprecated/old project viewer/TestViewer.ps1 new file mode 100644 index 0000000..e1bf6f4 --- /dev/null +++ b/deprecated/old project viewer/TestViewer.ps1 @@ -0,0 +1,126 @@ +# cd .\Projects\PowerRemoteDesktop\; IEX (Get-Content .\TestViewer.ps1 -Raw -Encoding UTF8) + +Write-Output "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️" +Write-Output "⚠️ Only use this script for testing the application NOT in production ⚠️" +Write-Output "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️" + +Invoke-Expression -Command (Get-Content "PowerRemoteDesktop_Viewer\PowerRemoteDesktop_Viewer.psm1" -Raw) + +# Different Scenarios + +$remoteHost = "127.0.0.1" +$password = "Jade@123@Pwd" + +Write-Host "Scenarios" +Write-Host "---------------" +Write-Host "1. Classic (Secure Password)" +Write-Host "2. Classic (Plain-text password) + LogonUI" +Write-Host "3. Always On Top, Disable Verbosity" +Write-Host "4. TLS v1.3" +Write-Host "5. Clipboard Receive" +Write-Host "6. Clipboard Send" +Write-Host "7. Clipboard Disabled" +Write-Host "8. Image Quality Really Bad" +Write-Host "9. Image Quality Bad" +Write-Host "10. Image Quality High" +Write-Host "11. Resize 10%" +Write-Host "12. Resize 80%, Packet Size 16KiB, BlockSize 128x128" +Write-Host "13. Bad Password" + +Write-Host "" + +[int]$scenario = Read-Host "Please choose scenario (default: 1)" + +switch ($scenario) +{ + 2 + { + Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -LogonUI + } + + 3 + { + Write-Host "⚡Check that verbosity is not shown." + Write-Host "⚡Check that virtual desktop form is above all windows." + + Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -DisableVerbosity -AlwaysOnTop + } + + 4 + { + Write-Host "⚡Check that TLSv1.3 is working." + + Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -UseTLSv1_3 + } + + 5 + { + Write-Host "⚡Check if viewer is only authorized to receive remote clipboard." + + Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -Clipboard "Receive" + } + + 6 + { + Write-Host "⚡Check if viewer is only authorized to send local clipboard." + + Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -Clipboard "Send" + } + + 7 + { + Write-Host "⚡Check if clipboard synchronization is completely disabled." + + Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -Clipboard "Disabled" + } + + 8 + { + Write-Host "⚡Check if image quality is really low." + + Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -ImageCompressionQuality 0 + } + + 9 + { + Write-Host "⚡Check if image quality is not really good." + + Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -ImageCompressionQuality 30 + } + + 10 + { + Write-Host "⚡Check if image quality is really good." + + Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -ImageCompressionQuality 100 + } + + 11 + { + Write-Host "⚡Check if desktop image is reduced by 10%." + Write-Host "⚡Check if resize quality is bad." + + Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -Resize -ResizeRatio 90 + } + + 12 + { + Write-Host "⚡Check if desktop image is reduced by 20%." + Write-Host "⚡Control block size." + Write-Host "⚡Control packet size." + + Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -Resize -ResizeRatio 80 -PacketSize "Size16384" -BlockSize "Size128" + } + + 13 + { + Write-Host "⚡Be sure that authentication fails with remote server." + + Invoke-RemoteDesktopViewer -Password "bad@Bad123!Bad" -ServerAddress $remoteHost + } + + default + { + Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String $password -AsPlainText -Force) -ServerAddress $remoteHost + } +} \ No newline at end of file