后台保活.ps1

<#PSScriptInfo
.VERSION 1.0.0
.GUID d64662e5-a2c2-4f12-a253-7195a7d59bec
.AUTHOR 埃博拉酱-机器人
.COMPANYNAME 一致行动党
.COPYRIGHT (c) 2026 埃博拉酱-机器人. MIT License.
.TAGS Session0 Background Process Persist Logoff RDP SSH WMI Windows
.LICENSEURI https://opensource.org/licenses/MIT
.RELEASENOTES
    1.0.0 - 初始版本。通过 SSH localhost + WMI 在 Session 0 创建保活进程。
#>


<#
.SYNOPSIS
    在 Session 0 创建登出后保活的后台进程
.DESCRIPTION
    通过 SSH localhost 让 sshd(Session 0 的 SYSTEM 服务)以当前用户身份创建进程,
    再用 WMI Win32_Process.Create 绕过 sshd 的 Job Object 使进程独立存活。
    进程运行在 Session 0,远程桌面登出后不会被终止。
 
    原理:
    1. SSH 连接 localhost → sshd 运行在 Session 0,故 SSH 会话继承 Session 0
    2. SSH 会话内通过 WMI 创建进程 → 进程由 wmiprvse.exe 派生,脱离 sshd Job Object
    3. SSH 断开后 WMI 创建的进程不受影响,持续运行在 Session 0
 
    首次使用自动生成 SSH 密钥并配置免密登录。
    若 sshd 未启动/被禁用则通过 UAC 请求一次性提权。
    sshd 未安装时报错提示安装命令。
 
    前置条件:
    - Windows 10 / Server 2019 或更高版本
    - OpenSSH 客户端(系统内置)
    - OpenSSH Server(可选功能,首次使用会提示安装)
.PARAMETER 命令行
    要在后台运行的完整命令行
.EXAMPLE
    .\后台保活.ps1 'powershell -NoProfile -File C:\Scripts\MyTask.ps1'
 
    在 Session 0 启动 PowerShell 脚本,RDP 登出后继续运行。
.EXAMPLE
    $PID = .\后台保活.ps1 'python C:\Scripts\server.py'
    Stop-Process -Id $PID
 
    启动 Python 服务器并记录 PID 以便后续停止。
.INPUTS
    None
.OUTPUTS
    System.Int32
    成功时返回后台进程的 PID。
.NOTES
    需要 OpenSSH Server 已安装。首次运行需 UAC 一次性提权以启动 sshd 服务。
.LINK
    https://learn.microsoft.com/windows-server/administration/openssh/openssh-install-firstuse
#>

param(
    [Parameter(Mandatory=$true, Position=0)]
    [string]$命令行
)

# ---------- 前置检查 ----------

if (-not (Get-Command ssh -ErrorAction SilentlyContinue)) {
    Write-Host "错误: 未找到 ssh 命令,请确认 OpenSSH 客户端已安装" -ForegroundColor Red
    return
}

$sshd服务 = Get-Service sshd -ErrorAction SilentlyContinue
if (-not $sshd服务) {
    Write-Host "错误: OpenSSH Server 未安装。请以管理员身份运行:" -ForegroundColor Red
    Write-Host " Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0" -ForegroundColor Yellow
    return
}

# ---------- 确保 SSH 密钥 ----------

$密钥目录 = "$env:USERPROFILE\.ssh"
$密钥路径 = "$密钥目录\id_ed25519"
$公钥路径 = "$密钥目录\id_ed25519.pub"

if (-not (Test-Path $密钥路径)) {
    if (-not (Test-Path $密钥目录)) { New-Item -ItemType Directory -Path $密钥目录 -Force | Out-Null }
    & ssh-keygen -t ed25519 -f "$密钥路径" -N '""' -q 2>&1 | Out-Null
    if (-not (Test-Path $密钥路径)) {
        Write-Host "错误: SSH 密钥生成失败" -ForegroundColor Red
        return
    }
    Write-Host "已生成 SSH 密钥" -ForegroundColor Green
}

# ---------- 测试 SSH 连接 ----------

$测试输出 = & ssh -n -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 localhost "echo __SSH_OK__" 2>&1
$已连通 = ($测试输出 | Out-String).Contains("__SSH_OK__")

# ---------- SSH 未就绪时一次性 UAC 配置 ----------

if (-not $已连通) {
    Write-Host "首次使用,需要管理员权限配置 SSH..." -ForegroundColor Yellow

    $公钥内容 = (Get-Content $公钥路径 -Raw).Trim()
    $标记文件 = Join-Path $env:TEMP "ssh-setup-done-$PID"
    Remove-Item $标记文件 -ErrorAction SilentlyContinue

    $提权脚本 = Join-Path $env:TEMP "ssh-setup-$PID.ps1"
    @"
`$ErrorActionPreference = 'Stop'
try {
    # 启动 sshd 并设为开机自启
    Start-Service sshd -ErrorAction SilentlyContinue
    Set-Service sshd -StartupType Automatic
 
    # 管理员用户的 authorized_keys
    `$管理员密钥 = 'C:\ProgramData\ssh\administrators_authorized_keys'
    `$公钥 = '$($公钥内容 -replace "'","''")'
    if (-not (Test-Path `$管理员密钥) -or -not (Get-Content `$管理员密钥 -Raw -ErrorAction SilentlyContinue).Contains(`$公钥)) {
        Add-Content `$管理员密钥 -Value `$公钥 -Encoding UTF8
    }
    icacls `$管理员密钥 /inheritance:r /grant 'SYSTEM:(R)' /grant 'Administrators:(R)' | Out-Null
 
    # 普通用户的 authorized_keys
    `$用户密钥 = '$密钥目录\authorized_keys'
    if (-not (Test-Path `$用户密钥) -or -not (Get-Content `$用户密钥 -Raw -ErrorAction SilentlyContinue).Contains(`$公钥)) {
        Add-Content `$用户密钥 -Value `$公钥 -Encoding UTF8
    }
 
    'OK' | Out-File '$标记文件'
} catch {
    `$_.Exception.Message | Out-File '$标记文件'
}
"@
 | Set-Content $提权脚本 -Encoding UTF8

    try {
        Start-Process powershell -Verb RunAs -Wait -ErrorAction Stop `
            -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$提权脚本`""
    } catch {
        Write-Host "错误: UAC 提权被取消" -ForegroundColor Red
        Remove-Item $提权脚本 -ErrorAction SilentlyContinue
        return
    }
    Remove-Item $提权脚本 -ErrorAction SilentlyContinue

    if (-not (Test-Path $标记文件)) {
        Write-Host "错误: 配置脚本未正常完成" -ForegroundColor Red
        return
    }
    $结果 = (Get-Content $标记文件 -Raw).Trim()
    Remove-Item $标记文件 -ErrorAction SilentlyContinue

    if ($结果 -ne 'OK') {
        Write-Host "错误: $结果" -ForegroundColor Red
        return
    }

    Start-Sleep 2
    $测试输出 = & ssh -n -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 localhost "echo __SSH_OK__" 2>&1
    $已连通 = ($测试输出 | Out-String).Contains("__SSH_OK__")

    if (-not $已连通) {
        Write-Host "错误: 配置完成但 SSH 连接仍失败" -ForegroundColor Red
        Write-Host ($测试输出 | Out-String) -ForegroundColor Gray
        return
    }
    Write-Host "SSH 配置完成" -ForegroundColor Green
}

# ---------- 通过 SSH + WMI 在 Session 0 创建保活进程 ----------
# 用 WMI Win32_Process.Create 绕过 sshd 的 Job Object,
# 使进程在 SSH 会话结束后仍然存活

$命令文件 = Join-Path $env:TEMP "bg-cmd-$PID.txt"
$启动器 = Join-Path $env:TEMP "bg-launch-$PID.ps1"

$命令行 | Set-Content $命令文件 -Encoding UTF8 -NoNewline

@"
`$cmd = (Get-Content '$命令文件' -Raw).Trim()
`$r = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{CommandLine = `$cmd}
if (`$r.ReturnValue -eq 0) { `$r.ProcessId } else { throw "WMI CreateProcess 失败: ReturnValue=`$(`$r.ReturnValue)" }
"@
 | Set-Content $启动器 -Encoding UTF8

$输出 = & ssh -n -o BatchMode=yes localhost "powershell -NoProfile -ExecutionPolicy Bypass -File `"$启动器`"" 2>&1
Remove-Item $命令文件, $启动器 -ErrorAction SilentlyContinue

$PID行 = $输出 | Where-Object { "$_".Trim() -match '^\d+$' } | Select-Object -First 1
if ($PID行) {
    $进程ID = [int]"$PID行".Trim()
    $进程 = Get-Process -Id $进程ID -ErrorAction SilentlyContinue
    if ($进程) {
        Write-Host "已在 Session $($进程.SessionId) 启动后台进程 (PID=$进程ID)" -ForegroundColor Green
    } else {
        Write-Host "已启动后台进程 (PID=$进程ID)" -ForegroundColor Green
    }
    return $进程ID
} else {
    Write-Host "错误: 启动后台进程失败" -ForegroundColor Red
    if ($输出) { $输出 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } }
}