Загрузка данных


# 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
}