如何使用powershell在远程桌面上截屏

Gan*_*esh 3 windows powershell screenshot remote-desktop

我写了一个代码,打开浏览器并截图。但是当我通过远程桌面运行它时,它会拍摄空白图像。

谁能建议一下,如何通过 PowerShell 在远程桌面上截取浏览器的屏幕截图?

例如,我需要在远程桌面上打开https://stackoverflow.com/并将屏幕截图保存在该远程服务器中。

代码

[Reflection.Assembly]::LoadWithPartialName("System.Drawing")
function screenshot([Drawing.Rectangle]$bounds, $path) {
   $bmp = New-Object Drawing.Bitmap $bounds.width, $bounds.height
   $graphics = [Drawing.Graphics]::FromImage($bmp)

   $graphics.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.size)

   $bmp.Save($path)

   $graphics.Dispose()
   $bmp.Dispose()
}
Run Code Online (Sandbox Code Playgroud)

Ben*_*est 5

您尝试执行的操作无法直接远程执行

当我在这里说“远程”时,我的意思是您无法从一个系统调用到另一个系统的屏幕捕获。该代码必须在本地上下文中执行。

最终,您需要$graphics.CopyFromScreen()从也登录到桌面的用户运行,而不仅仅是通过PSRemoting会话。实际上没有任何 GUI 方面的东西可以捕获;远程处理会话没有在另一端加载 GUI 组件。简而言之,您不能指望使用它PSRemoting来捕获远程屏幕截图。它没有发生,没有可以复制的屏幕,并且应该导致Win32Exception:“句柄无效”。每当您尝试从会话复制屏幕缓冲区(屏幕缓冲区与显示缓冲区不同)时PSRemoting,即使用户同时通过 RDP 登录,也会发生这种情况。

注意:我的测试表明屏幕捕获仍应通过 RDP 工作,而无需修改现有代码。我不确定是否有 GPO 或其他设置可以阻止此操作。

但是,如果登录会话存在但处于非活动状态(例如 RDP 客户端已断开连接),您将收到相同的“无效句柄”错误。这个答案进一步阐明了该行为,但基本上您需要有一个活动的用户会话才能从中复制绘制上下文。如果没有这个,就没有绘制上下文,因此就没有有效的句柄来获取图形数据。

有一个可能的解决方案,使用 Win32 API 作为另一个用户执行交互式登录,然后执行 PowerShell 脚本(也使用 Win32 API 调用),我在此答案中进一步了解了更多信息。


只要目标用户会话处于活动状态即可解决此问题

要解决此问题,您必须使用活动会话模拟已登录的桌面主体,并在本地上下文中运行脚本。如果您用于PSRemoting建立远程连接,请设置计划任务来运行脚本以作为目标登录用户捕获屏幕(您的脚本还应将屏幕截图写入某处的临时文件中):

注意:如果您可能需要捕获多个用户的桌面,则您将需要为每个用户分配一个任务,或者在运行任务之前为目标用户重新配置该任务。

$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-File C:\path\to\ScreenShotScript.ps1"
$trigger = New-ScheduledTaskTrigger -Once
$principal = "DomainOrComputerName\AccountName"
$settings = New-ScheduledTaskSettingsSet
$task = New-ScheduledTask -Action $action -Principal $principal -Trigger $trigger -Settings $settings
Register-ScheduledTask TakeScreenShot -InputObject $task
Run Code Online (Sandbox Code Playgroud)

然后当你想运行它时调用该任务:

Start-ScheduledTask -TaskName TakeScreenShot
while( ( Get-ScheduledTask -TaskName TakeScreenShot ).State -ne 'Ready' ) {
  # Wait until the screencap task finishes
  Start-Sleep 1
}
Run Code Online (Sandbox Code Playgroud)

然后从您要远程处理的主机会话(请注意,您的会话应存储为变量以使此副本正常工作):

Copy-Item -FromSession $psRemotingSession C:\path\to\screenshot.png C:\path\to\save\screenshot\locally\to
Run Code Online (Sandbox Code Playgroud)

如果您的最终目标是连接到 RDP 并使用 PowerShell 启动站点,我可以分享以下功能,以通过 PowerShell 启动远程 RDP 连接。然后,您可以使用上面的任务计划程序解决方法启动浏览器到正确的站点并执行远程捕获,不过您也可以在关闭 RDP 窗口之前截取它的屏幕截图:

注意:此函数返回$True是否mstsc启动,但不检查连接成功的结果。$False如果由于某种原因无法存储凭证,它将返回。

function Connect-RDP {
  Param(
    [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
    [string]$Hostname,
    [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
    [ushort]$Port,
    [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
    [pscredential]$Credential
  )

  $cmdkeyArgs =
    "/generic:TERMSRV/${Hostname}",
    "/user:$($Credential.Username)",
    "/pass:$($Credential.GetNetworkCredential().Password)"
  cmdkey @cmdkeyArgs

  if ( $LASTEXITCODE -ne 0 ) {
    throw "cmdkey failed with exit code ${LASTEXITCODE}"
  }

  $mstscArgs = ,
    "/v:${Hostname}:${Port}"
  mstsc @mstscArgs
}
Run Code Online (Sandbox Code Playgroud)

此函数还可用于与目标用户启动到目标计算机的活动 RDP 会话,因此您现有的屏幕截图代码将起作用。完成后请记住关闭窗口。请注意,如果目标用户已经远程或本地登录,这会将其踢出。

理论上,您还可以通过任务计划程序运行的代码来操作浏览器窗口,但您可能会更好地通过本地会话中的 RDP 窗口来操作浏览器窗口,除非您依赖于诸如Selenium.

我上面链接的答案还提供了一些您可以尝试的其他解决方法,包括您可以访问的APIUser32中的其他功能。但是,这些解决方法可能会迫使您重新构建当前的自动化解决方案。请注意,此答案来自 2012 年,从服务捕获屏幕缓冲区的建议可能不再适用,因为在 Windows Vista 的生命周期中,授予服务访问用户桌面的能力已被悄悄撤销。Win32P/Invoke


附加信息

CopyFromScreen您可以从.NET Core 源代码了解其内部工作原理以及User32.GetDC(IntPtr)它用于更好地理解此行为的函数。


如果您想尝试AdvApi.CreateProcessAsUser使用 Win32 API,我已经编写了一个函数,它将提供P/Invoke必要的 Win32 API 函数并在当前的 PowerShell 会话中创建依赖项数据结构。不幸的是,我在开始LogonUser工作时遇到了一些困难,因此我没有实际执行登录并以另一个用户身份启动进程的工作示例,但此函数至少可以在 PowerShell 会话中获取可用的函数。

以下是在会话中定义该函数后如何使用该函数的方法:

# This function will only work once per session (assuming Add-Type doesn't errorout )
PInvoke-AdvApi32

# I don't have a working example but call both functions as static methods on `[AdvApi32]` like so
[AdvApi32]::LogonUser(....)
[AdvApi32]::CreateProcessAsUser(....)
Run Code Online (Sandbox Code Playgroud)

有关如何使用这些函数的正确示例,您可以参考CreateProcessAsUserLogonUserP/Invoke的文档。请注意,示例采用 C# 和 VB.NET 语言,因此您必须自行将示例转换为 .NET。如果您可以开始工作,即使在通过远程连接时,您也应该能够将 PowerShell 代码作为目标用户的新进程调用。LogonUser

如果您尝试此路线,可能还有其他AdvApi32函数对您有用,如果您决定使用P/Invoke其他方法,请注意您可以粘贴到函数中的 C# 代码定义中,确保将任何访问修饰符更改为public(它们通常定义为如internalC# 示例中所示)。您还需要定义的任何其他数据结构将在该站点的任何页面上提及P/Invoke


总结中

User32GetDC(IntPtr)函数获取指定窗口句柄的绘图上下文,或整个桌面(如果0提供)。Graphics.CopyFromScreen此功能行为对于正常运行至关重要。正如上面进一步提到的,主要问题是当您通过 进行连接时没有绘图上下文PSRemoting,如果您在登录会话不处于活动状态时使用任务计划程序解决方法,也不会出现绘图上下文。因此,无论您使用Graphics.CopyFromScreen还是尝试重写在上面链接的 .NET Core 源代码中找到的某些内容,如果出现以下情况,您都无法复制用户的屏幕缓冲区:

  • 用户已注销。
  • 用户已登录,但会话处于非活动状态(例如,RDP 客户端断开连接、屏幕锁定等)。
  • 用户会话处于活动状态,但您正尝试通过会话复制屏幕缓冲区PSRemoting,因为PSRemoting不允许远程用户访问远程计算机上的图形上下文。
  • Win32 API 有一个潜在的解决方案,但它不适合胆小的人。

PInvoke-AdvApi32 函数

这是来源PInvoke-AdvApi32

Function PInvoke-AdvApi32 {
    Param(
        [string]$ExposedClassName = 'AdvApi32',
        [switch]$PassThru
    )
    
    #region CSDefinitions

    # Define the C# code we need to import
    # ... yes this function needs several definitions
    $pinvokeDefinitions = 
    @"
using System;
using System.Runtime.InteropServices;

[Flags]
public enum CreateProcessFlags
{
    CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
    CREATE_DEFAULT_ERROR_MODE = 0x04000000,
    CREATE_NEW_CONSOLE = 0x00000010,
    CREATE_NEW_PROCESS_GROUP = 0x00000200,
    CREATE_NO_WINDOW = 0x08000000,
    CREATE_PROTECTED_PROCESS = 0x00040000,
    CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
    CREATE_SEPARATE_WOW_VDM = 0x00000800,
    CREATE_SHARED_WOW_VDM = 0x00001000,
    CREATE_SUSPENDED = 0x00000004,
    CREATE_UNICODE_ENVIRONMENT = 0x00000400,
    DEBUG_ONLY_THIS_PROCESS = 0x00000002,
    DEBUG_PROCESS = 0x00000001,
    DETACHED_PROCESS = 0x00000008,
    EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
    INHERIT_PARENT_AFFINITY = 0x00010000
}

public enum LOGON_PROVIDER
{
     LOGON32_PROVIDER_DEFAULT,
     LOGON32_PROVIDER_WINNT35,
     LOGON32_PROVIDER_WINNT40,
     LOGON32_PROVIDER_WINNT50
}

public enum LOGON_TYPE
{
     LOGON32_LOGON_INTERACTIVE = 2,
     LOGON32_LOGON_NETWORK = 3,
     LOGON32_LOGON_BATCH = 4,
     LOGON32_LOGON_SERVICE = 5,
     LOGON32_LOGON_UNLOCK = 7,
     LOGON32_LOGON_NETWORK_CLEARTEXT = 8,
     LOGON32_LOGON_NEW_CREDENTIALS = 9
}

// This also works with CharSet.Ansi as long as the calling function uses the same character set.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct STARTUPINFOEX
{
    public STARTUPINFO StartupInfo;
    public IntPtr lpAttributeList;
}

// If you are using this with [GetStartupInfo], this definition works without errors.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct STARTUPINFO
{
    public Int32 cb;
    public IntPtr lpReserved;
    public IntPtr lpDesktop;
    public IntPtr lpTitle;
    public Int32 dwX;
    public Int32 dwY;
    public Int32 dwXSize;
    public Int32 dwYSize;
    public Int32 dwXCountChars;
    public Int32 dwYCountChars;
    public Int32 dwFillAttribute;
    public Int32 dwFlags;
    public Int16 wShowWindow;
    public Int16 cbReserved2;
    public IntPtr lpReserved2;
    public IntPtr hStdInput;
    public IntPtr hStdOutput;
    public IntPtr hStdError;
}

[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
    public IntPtr hProcess;
    public IntPtr hThread;
    public int dwProcessId;
    public int dwThreadId;
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public unsafe byte* lpSecurityDescriptor;
    public int bInheritHandle;
}

public enum LogonProvider
{
    /// <summary>
    /// Use the standard logon provider for the system.
    /// The default security provider is negotiate, unless you pass NULL for the domain name and the user name
    /// is not in UPN format. In this case, the default provider is NTLM.
    /// NOTE: Windows 2000/NT:   The default security provider is NTLM.
    /// </summary>
    LOGON32_PROVIDER_DEFAULT = 0,
    LOGON32_PROVIDER_WINNT35 = 1,
    LOGON32_PROVIDER_WINNT40 = 2,
    LOGON32_PROVIDER_WINNT50 = 3
}

public class ${ExposedClassName} {
    [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
    public static extern bool CreateProcessAsUser(
        IntPtr hToken,
        string lpApplicationName,
        string lpCommandLine,
        ref SECURITY_ATTRIBUTES lpProcessAttributes,
        ref SECURITY_ATTRIBUTES lpThreadAttributes,
        bool bInheritHandles,
        uint dwCreationFlags,
        IntPtr lpEnvironment,
        string lpCurrentDirectory,
        ref STARTUPINFO lpStartupInfo,
        out PROCESS_INFORMATION lpProcessInformation);

    [DllImport("advapi32.dll", SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool LogonUser(
        [MarshalAs(UnmanagedType.LPStr)] string pszUserName,
        [MarshalAs(UnmanagedType.LPStr)] string pszDomain,
        [MarshalAs(UnmanagedType.LPStr)] string pszPassword,
        int dwLogonType,
        int dwLogonProvider,
        ref IntPtr phToken);
}
"@
    #endregion CSDefinitions

    # Compile and load our heroic Win32 helper class and definitions
    if ( !( [System.Management.Automation.PSTypeName]$ExposedClassName ).Type ) {
        Write-Host "Adding type ""${ExposedClassName}"""
        $addTypeParams = @{
            TypeDefinition        = $pinvokeDefinitions
            CompilerParameters    = New-Object System.CodeDom.Compiler.CompilerParameters -Property @{
                CompilerOptions = '/unsafe'
            }
            PassThru = $PassThru
            ErrorAction = 'Stop'
        }
    
        Add-Type @addTypeParams
    } else {
        Write-Warning "AdvApi32 has already been P/Invoked. If you need to P/Invoke this class again function again, you must start a new PowerShell session."
        Write-Warning "Changing the -ExposedClassName will bypass this check but Add-Type will fail on dependent definitions without a unique name."
    }
}
Run Code Online (Sandbox Code Playgroud)