Загрузка данных
# REBUILD THE CONFIRMED DAMAGED RDP LISTENER ON WINDOWS SERVER 2022 (20348).
# Run in elevated 64-bit Windows PowerShell ON THE VPS, through provider console/VNC.
# Backups first; replaces ONLY the stripped RDP-Tcp key using the native Windows provider.
# Restarts RDP. No reboot/downloads/firewall or certificate changes.
& {
function Get-RdpListenerSnapshot {
$path='HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
if(-not (Test-Path -LiteralPath $path)){throw 'RDP-Tcp is absent; this targeted patch expects the reported damaged key.'}
$key=Get-Item -LiteralPath $path -ErrorAction Stop
try {
$values=@()
foreach($name in @($key.GetValueNames() | Sort-Object)){
$values += [pscustomobject]@{Name=$name;Kind=$key.GetValueKind($name).ToString();Value=$key.GetValue($name,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)}
}
[pscustomobject]@{Values=$values;SubKeys=@($key.GetSubKeyNames())}
} finally {$key.Close()}
}
function Test-RdpStrippedSnapshot {
param($Snapshot)
# Only replace the exact kind of stub reported by this user. Do not replace
# a configured listener, a custom provider, certificates, or nested keys.
$allowed=@('PortNumber','fEnableWinStation','fLogonDisabled','UserAuthentication','SecurityLayer','MinEncryptionLevel')
if($Snapshot.SubKeys.Count -ne 0 -or $Snapshot.Values.Count -gt $allowed.Count){return $false}
foreach($value in $Snapshot.Values){if($value.Name -notin $allowed -or $value.Kind -ne 'DWord'){return $false}}
return $true
}
function Assert-RdpProviderSuccess {
param($Result,[string]$Operation)
if($null -eq $Result -or $null -eq $Result.ReturnValue){throw ($Operation+': provider returned no status code.')}
if([uint32]$Result.ReturnValue -ne 0){throw ('{0} failed: {1} (0x{1:X8})' -f $Operation,[uint32]$Result.ReturnValue)}
}
function Get-RdpNativeProvider {
$namespace='root/cimv2/terminalservices'
$class=Get-CimClass -Namespace $namespace -ClassName Win32_TerminalServiceSetting -OperationTimeoutSec 20 -ErrorAction Stop
foreach($method in @('GetWinstationDriverNames','GetTSLanaIds','CreateWinstation')){
if($null -eq $class.CimClassMethods[$method]){throw ('Windows RDP provider has no '+$method+'. No registry configuration changed.')}
}
$instances=@(Get-CimInstance -Namespace $namespace -ClassName Win32_TerminalServiceSetting -OperationTimeoutSec 20 -ErrorAction Stop)
if($instances.Count -ne 1){throw 'Expected one local TerminalServiceSetting provider instance.'}
$provider=$instances[0]
$drivers=Invoke-CimMethod -InputObject $provider -MethodName GetWinstationDriverNames -OperationTimeoutSec 20 -ErrorAction Stop
Assert-RdpProviderSuccess $drivers 'GetWinstationDriverNames'
$template=Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\Wds\rdpwd' -ErrorAction Stop
$driver=[string]$template.WdName
$protocol=[guid]::Empty
if([string]::IsNullOrWhiteSpace($driver) -or $driver -notin @($drivers.WinstaDriverNames)){
throw ('The installed rdpwd template does not match an available driver. Available: '+(@($drivers.WinstaDriverNames) -join ', '))
}
if(-not [guid]::TryParse([string]$template.LoadableProtocol_Object,[ref]$protocol)){
throw 'The local rdpwd template is also damaged: LoadableProtocol_Object is absent/invalid. A matching Windows backup is required.'
}
$adapters=Invoke-CimMethod -InputObject $provider -MethodName GetTSLanaIds -OperationTimeoutSec 20 -ErrorAction Stop
Assert-RdpProviderSuccess $adapters 'GetTSLanaIds'
if(0 -notin @($adapters.LanaIdList)){throw 'The provider does not advertise the all-adapters LANA 0 binding. No adapter is selected by guesswork.'}
[pscustomobject]@{Instance=$provider;Driver=$driver;Protocol=$protocol.ToString('B');LanaId=[uint32]0}
}
function New-RdpProviderListener {
param($Provider)
Invoke-CimMethod -InputObject $Provider.Instance -MethodName CreateWinstation -Arguments @{
Name='RDP-Tcp';WinstaDriverName=$Provider.Driver;LanaId=[uint32]$Provider.LanaId
} -OperationTimeoutSec 30 -ErrorAction Stop
}
function Assert-RdpRebuiltStructure {
param($Snapshot,[string]$ExpectedProtocol)
foreach($name in @('LoadableProtocol_Object','WdName','PdName')){
$entry=@($Snapshot.Values | Where-Object Name -eq $name)
if($entry.Count -ne 1 -or $entry[0].Kind -ne 'String' -or [string]::IsNullOrWhiteSpace([string]$entry[0].Value)){
throw ('Windows did not recreate a valid '+$name+'.')
}
}
$actual=[guid]::Empty
$entry=@($Snapshot.Values | Where-Object Name -eq 'LoadableProtocol_Object')[0]
if(-not [guid]::TryParse([string]$entry.Value,[ref]$actual) -or $actual -ne [guid]$ExpectedProtocol){throw 'Recreated protocol does not match the installed Windows rdpwd template.'}
$adapter=@($Snapshot.Values | Where-Object Name -eq 'LanAdapter')
if($adapter.Count -ne 1 -or $adapter[0].Kind -ne 'DWord' -or $adapter[0].Value -ne 0){throw 'Windows did not recreate the requested network adapter binding.'}
}
function Remove-RdpTargetKey {
# Intentionally a literal LEAF path; never remove Terminal Server/WinStations.
Remove-Item -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Recurse -Force -ErrorAction Stop
}
function Import-RdpTargetBackup {
param([string]$File,[string]$Hash)
if((Get-FileHash -LiteralPath $File -Algorithm SHA256).Hash -ne $Hash){throw 'Backup hash mismatch. Automatic rollback refused.'}
# File is the exact reg.exe export made before removing this leaf key.
if(Test-Path -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'){Remove-RdpTargetKey}
$output=& "$env:SystemRoot\System32\reg.exe" import $File 2>&1
if($LASTEXITCODE -ne 0){throw ('Listener backup import failed: '+($output -join ' '))}
}
function Invoke-RdpListener2022Restore {
$ErrorActionPreference='Stop'
$script:RdpRecoveryLog=$null
$directory=$null; $removed=$false; $committed=$false; $providerPending=$false
$backupFile=$null; $backupHash=$null; $beforeJson=$null
try {
$principal=New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if(-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)){throw 'Run from elevated Windows PowerShell ON THE AFFECTED VPS.'}
if(-not [Environment]::Is64BitProcess){throw 'Use 64-bit Windows PowerShell.'}
$version=Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop
if([string]$version.CurrentBuild -ne '20348' -or [string]$version.InstallationType -notlike 'Server*'){
throw 'This patch is restricted to Windows Server 2022 build 20348. No configuration changed.'
}
$directory=Join-Path $env:ProgramData ('WinInstallBuilder\RdpRebuild\'+(Get-Date -Format 'yyyyMMdd-HHmmss')+'-'+[guid]::NewGuid().ToString('N'))
New-Item -Path $directory -ItemType Directory -ErrorAction Stop | Out-Null
$script:RdpRecoveryLog=Join-Path $directory 'rebuild.log'
Write-RdpLog ('Server 2022 listener rebuild. Windows '+$version.CurrentBuild+'.'+$version.UBR+'; backup/log: '+$directory)
$before=Get-RdpListenerSnapshot
$beforeJson=ConvertTo-Json -InputObject $before -Depth 8 -Compress
$before | Export-Clixml -LiteralPath (Join-Path $directory 'listener-before.xml')
if(-not (Test-RdpStrippedSnapshot $before)){
throw 'Listener is not the reported six-value stub (or was already rebuilt). Nothing overwritten. Inspect listener-before.xml.'
}
$desired=@{PortNumber=3389;fEnableWinStation=1;fLogonDisabled=0;UserAuthentication=1;SecurityLayer=1;MinEncryptionLevel=3}
foreach($entry in $before.Values){
switch($entry.Name){
'PortNumber' {if($entry.Value -ge 1 -and $entry.Value -le 65535){$desired.PortNumber=[int]$entry.Value}}
'UserAuthentication' {if($entry.Value -in @(0,1)){$desired.UserAuthentication=[int]$entry.Value}}
'SecurityLayer' {if($entry.Value -in @(0,1,2)){$desired.SecurityLayer=[int]$entry.Value}}
'MinEncryptionLevel' {if($entry.Value -in @(1,2,3,4)){$desired.MinEncryptionLevel=[int]$entry.Value}}
}
}
$tasks=@(Get-ScheduledTask -TaskPath '\' -ErrorAction Stop | Where-Object TaskName -eq 'WinRdpNla')
if(@($tasks | Where-Object {$_.Settings.Enabled -or $_.State -eq 'Running'}).Count){throw 'WinRdpNla is still enabled/running. Disable the known old installer reset task before rebuilding.'}
$root='HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server'
if((Get-ItemProperty -LiteralPath $root -Name fDenyTSConnections).fDenyTSConnections -ne 0){throw 'RDP is disabled at the server level. This patch only rebuilds the listener.'}
$policy='HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services'
if(Test-Path -LiteralPath $policy){
$policyValues=Get-ItemProperty -LiteralPath $policy
if($null -ne $policyValues.fDenyTSConnections -and $policyValues.fDenyTSConnections -ne 0){throw 'RDP is blocked by policy. The policy was not changed.'}
}
$service=Get-CimInstance Win32_Service -Filter "Name='TermService'" -OperationTimeoutSec 15 -ErrorAction Stop
if($service.State -ne 'Running'){throw 'TermService must be running for the native provider. No listener configuration changed.'}
$tcp=@(Get-NetTCPConnection -State Listen -ErrorAction Stop)
if(@($tcp | Where-Object {$_.LocalPort -eq $desired.PortNumber -or ($_.OwningProcess -eq $service.ProcessId -and $service.ProcessId -gt 0)}).Count){
throw 'Target port or another TermService listener is already active. Refusing to replace it automatically.'
}
$provider=Get-RdpNativeProvider
Write-RdpLog ('Native provider ready; installed driver: '+$provider.Driver+'; protocol: '+$provider.Protocol)
$backupFile=Join-Path $directory 'RDP-Tcp-before.reg'
Export-RdpRegistryKey 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' $backupFile
if(-not (Test-Path -LiteralPath $backupFile) -or (Get-Item -LiteralPath $backupFile).Length -lt 64){throw 'Listener backup is missing/empty. No configuration changed.'}
$backupHash=(Get-FileHash -LiteralPath $backupFile -Algorithm SHA256).Hash
Export-RdpRegistryKey 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server' (Join-Path $directory 'Terminal-Server-before.reg')
if((ConvertTo-Json -InputObject (Get-RdpListenerSnapshot) -Depth 8 -Compress) -cne $beforeJson){throw 'Listener changed during preflight. Aborting before removal.'}
Write-RdpLog 'Backup complete. Replacing ONLY the confirmed stripped RDP-Tcp key using Windows CreateWinstation.'
Remove-RdpTargetKey
$removed=$true
$providerPending=$true
$created=New-RdpProviderListener $provider
$providerPending=$false
Assert-RdpProviderSuccess $created 'CreateWinstation'
$rebuilt=Get-RdpListenerSnapshot
Assert-RdpRebuiltStructure $rebuilt $provider.Protocol
Write-RdpLog ('Windows recreated '+$rebuilt.Values.Count+' listener values. Restoring the selected port and preserved NLA/TLS settings.')
$path=$root+'\WinStations\RDP-Tcp'
foreach($name in $desired.Keys){
New-ItemProperty -LiteralPath $path -Name $name -PropertyType DWord -Value $desired[$name] -Force -ErrorAction Stop | Out-Null
}
$after=Get-RdpListenerSnapshot
Assert-RdpRebuiltStructure $after $provider.Protocol
foreach($name in $desired.Keys){
$check=@($after.Values | Where-Object Name -eq $name)
if($check.Count -ne 1 -or $check[0].Kind -ne 'DWord' -or $check[0].Value -ne $desired[$name]){throw ('Listener readback mismatch: '+$name)}
}
$committed=$true
$after | Export-Clixml -LiteralPath (Join-Path $directory 'listener-after.xml')
Export-RdpRegistryKey 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' (Join-Path $directory 'RDP-Tcp-after.reg')
Write-RdpLog 'Restarting RDP services; the console remains available. No OS reboot, firewall/policy/certificate changes.'
Restart-RdpAccessServices
$probe=$null
for($attempt=1;$attempt -le 10;$attempt++){
try {$probe=Test-RdpLocalProtocol $desired.PortNumber;break}
catch {
Write-RdpLog ('Local check '+$attempt+'/10: '+$_.Exception.Message)
if($attempt -eq 10){throw}
Start-Sleep -Seconds 3
}
}
Write-RdpLog ('LOCAL_RDP_RESPONDING: '+$probe+'. Connect from your PC to <SERVER_IP>:'+$desired.PortNumber)
Write-RdpLog 'Local negotiation only: external routing/firewall, certificates and account sign-in still require your connection test.'
[pscustomobject]@{Status='LOCAL_RDP_RESPONDING';Port=$desired.PortNumber;Report=$directory}
} catch {
$failure=$_.Exception.Message
if($removed -and -not $committed -and -not $providerPending){
try {
Import-RdpTargetBackup $backupFile $backupHash
if((ConvertTo-Json -InputObject (Get-RdpListenerSnapshot) -Depth 8 -Compress) -cne $beforeJson){throw 'Rollback readback differs from original snapshot.'}
Write-RdpLog 'Original damaged listener restored from the verified backup. Native recreation was not successful.'
} catch {Write-RdpLog ('ROLLBACK_FAILED: '+$_.Exception.Message+'. Keep the console open. Backup: '+$backupFile)}
} elseif($providerPending){
Write-RdpLog 'PROVIDER_RESULT_UNKNOWN: the provider call failed/timed out without a final result. No competing delete/rollback was issued. Keep the console open; backup is preserved.'
} elseif($committed){
Write-RdpLog 'Recreated listener preserved for further diagnosis; not replaced with the known damaged stub.'
}
Write-RdpLog ('REBUILD_INCOMPLETE: '+$failure)
if($directory){Write-RdpLog ('Send rebuild.log from '+$directory)}
[pscustomobject]@{Status='REBUILD_INCOMPLETE';Error=$failure;Report=$directory}
}
}
# Shared tested logging, backup, service restart and RDP negotiation functions.
function Write-RdpLog {
param([string]$Message)
$line = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' ' + $Message
Write-Host $line
if ($script:RdpRecoveryLog) { Add-Content -LiteralPath $script:RdpRecoveryLog -Value $line -Encoding UTF8 }
}
function Export-RdpRegistryKey {
param([string]$NativeKey, [string]$Destination)
$output = & "$env:SystemRoot\System32\reg.exe" export $NativeKey $Destination /y 2>&1
if ($LASTEXITCODE -ne 0) { throw ('Registry backup failed: ' + $NativeKey + '. ' + ($output -join ' ')) }
}
function Restart-RdpAccessServices {
$service = Get-Service -Name TermService -ErrorAction Stop
$dependents = @($service.DependentServices | Where-Object Status -eq 'Running' | Select-Object -ExpandProperty Name)
Set-Service -Name TermService -StartupType Automatic -ErrorAction Stop
if ($service.Status -ne 'Stopped') {
Stop-Service -Name TermService -Force -NoWait -ErrorAction Stop
$service.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Stopped, [timespan]::FromSeconds(25))
}
try {
$service.Start()
$service.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Running, [timespan]::FromSeconds(25))
} finally {
foreach ($name in $dependents) {
try { (Get-Service -Name $name -ErrorAction Stop).Start() }
catch { Write-RdpLog ('WARNING: dependent service ' + $name + ': ' + $_.Exception.Message) }
}
}
$redirector = Get-CimInstance Win32_Service -Filter "Name='UmRdpService'" -OperationTimeoutSec 15 -ErrorAction Stop
if ($redirector.StartMode -eq 'Disabled') { Set-Service -Name UmRdpService -StartupType Manual -ErrorAction Stop }
$redirectorService = Get-Service -Name UmRdpService -ErrorAction Stop
if ($redirectorService.Status -eq 'Stopped') { $redirectorService.Start() }
}
function Read-RdpPacket {
param([System.IO.Stream]$Stream)
$deadline = [DateTime]::UtcNow.AddSeconds(10)
$header = New-Object byte[] 4
$offset = 0
while ($offset -lt 4) {
if ([DateTime]::UtcNow -gt $deadline) { throw 'RDP response deadline exceeded.' }
$read = $Stream.Read($header, $offset, 4 - $offset)
if ($read -le 0) { throw 'RDP closed the connection before the TPKT header.' }
$offset += $read
}
$length = [int]$header[2] * 256 + [int]$header[3]
if ($header[0] -ne 3 -or $header[1] -ne 0 -or $length -lt 11 -or $length -gt 4096) { throw 'Invalid RDP TPKT response.' }
$packet = New-Object byte[] $length
[Array]::Copy($header, $packet, 4)
$offset = 4
while ($offset -lt $length) {
if ([DateTime]::UtcNow -gt $deadline) { throw 'RDP response deadline exceeded.' }
$read = $Stream.Read($packet, $offset, $length - $offset)
if ($read -le 0) { throw 'Truncated RDP response.' }
$offset += $read
}
if ($packet[5] -ne 0xD0 -or $packet[4] -ne ($length - 5)) { throw 'Response is not an RDP X.224 connection confirmation.' }
if ($length -eq 11) { return 'Standard RDP (legacy security; preserved, not enabled by this repair)' }
if ($length -ne 19 -or $packet[13] -ne 8 -or $packet[14] -ne 0) { throw 'Unexpected RDP negotiation structure.' }
$code = [BitConverter]::ToUInt32($packet, 15)
if ($packet[11] -eq 3) { throw ('RDP negotiation rejected; failure code=' + $code + '. No TLS/NLA downgrade attempted.') }
if ($packet[11] -ne 2 -or $code -notin @(0,1,2,8)) { throw 'Unexpected RDP negotiation protocol.' }
return ('RDP negotiation accepted; selectedProtocol=' + $code)
}
function Test-RdpLocalProtocol {
param([int]$Port)
$service = Get-CimInstance Win32_Service -Filter "Name='TermService'" -OperationTimeoutSec 15 -ErrorAction Stop
$listeners = @(Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object LocalPort -eq $Port)
$owned = @($listeners | Where-Object { $_.OwningProcess -eq $service.ProcessId -and $service.ProcessId -gt 0 })
if ($service.State -ne 'Running' -or -not $owned.Count) { throw ('TermService is not listening on TCP ' + $Port) }
# Test the actual bound address; do not assume IPv4 or all-interface binding.
$address = [string]$owned[0].LocalAddress
if ($address -eq '0.0.0.0') { $address = '127.0.0.1' }
if ($address -eq '::') { $address = '::1' }
$ip = [System.Net.IPAddress]::Parse($address)
$client = New-Object System.Net.Sockets.TcpClient($ip.AddressFamily)
$pending = $null
try {
$pending = $client.BeginConnect($ip, $Port, $null, $null)
if (-not $pending.AsyncWaitHandle.WaitOne(3000)) { throw 'Local TCP connection timed out.' }
$client.EndConnect($pending)
$stream = $client.GetStream()
$stream.ReadTimeout = 3000
$stream.WriteTimeout = 3000
# X.224 CR + RDP_NEG_REQ: TLS, CredSSP and CredSSP extended. No credentials.
[byte[]]$request = @(3,0,0,19,14,224,0,0,0,0,0,1,0,8,0,11,0,0,0)
$stream.Write($request, 0, $request.Length)
return (Read-RdpPacket $stream)
} finally {
$client.Close()
if ($pending) { $pending.AsyncWaitHandle.Close() }
}
}
Invoke-RdpListener2022Restore
}