# 2b V6/V7 proof target: a dedicated WinForms window we own. # ASCII-only source (PS 5.1 decodes BOM-less .ps1 as GBK; see personal lessons). # # Launched by text_injection_proof.ps1. Shows a topmost empty TextBox, writes # its current text to ResultFile (UTF-8 with BOM) on every change, exits when # StopFile appears. The proof injects through the remote-control pipeline and # polls ResultFile - so "what arrived on the controlled desktop" is reported # by the receiving application itself, not by a screenshot or OCR. param( [Parameter(Mandatory = $true)][string]$PidFile, [Parameter(Mandatory = $true)][string]$ResultFile, [Parameter(Mandatory = $true)][string]$StopFile, # SendInput lands on whatever is FOREGROUND. When the injector is driven # from another machine (phone -> agent -> this desktop), nothing in this # process keeps our window on top, so an unrelated window could eat the # injection. With this switch the form re-asserts foreground every tick and # the caller still asserts foreground before injecting. [switch]$GrabForeground ) $ErrorActionPreference = "Stop" Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing # Event handler scriptblocks resolve script-scoped vars; make that explicit # instead of relying on the param locals staying on a visible scope chain. $script:PidFile = $PidFile $script:ResultFile = $ResultFile $script:StopFile = $StopFile $script:lastWritten = $null $form = New-Object System.Windows.Forms.Form $form.Text = "RemoteTextTarget" $form.ClientSize = New-Object System.Drawing.Size(420, 140) $form.TopMost = $true $form.StartPosition = "Manual" $form.Location = New-Object System.Drawing.Point(80, 80) $tb = New-Object System.Windows.Forms.TextBox $tb.Multiline = $true $tb.Font = New-Object System.Drawing.Font("Segoe UI", 16.0) $tb.Dock = "Fill" $form.Controls.Add($tb) $script:GrabForeground = $GrabForeground if ($script:GrabForeground) { Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; public static class OwnForeground { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, IntPtr pid); [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId(); [DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f); // Deliberately NO keybd_event(VK_MENU). The usual 'tap ALT to beat the // foreground lock' trick corrupts exactly what we measure: after ALT, Windows // treats the next character as a menu MNEMONIC, so the first injected // character is eaten. Measured 2026-10-30: ALT re-asserting every 60ms turned // three injected characters into only 'b' in the target. // Attaching input queues is a lock workaround that synthesises no key input. public static bool Claim(IntPtr h) { uint fgThread = GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero); uint myThread = GetCurrentThreadId(); AttachThreadInput(myThread, fgThread, true); bool ok = SetForegroundWindow(h); AttachThreadInput(myThread, fgThread, false); return ok && GetForegroundWindow() == h; } public static IntPtr Now() { return GetForegroundWindow(); } } "@ } $timer = New-Object System.Windows.Forms.Timer $timer.Interval = 150 $timer.Add_Tick({ if (Test-Path $script:StopFile) { $form.Close() return } if ($script:GrabForeground) { # Re-assert only when actually lost (unconditional claiming every tick # would fight the user for the desktop outside a proof run). if ([OwnForeground]::Now() -ne $form.Handle) { [void]$form.Activate() [void][OwnForeground]::Claim($form.Handle) } } if ($tb.Text -ne $script:lastWritten) { $script:lastWritten = $tb.Text $enc = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllText($script:ResultFile, $tb.Text, $enc) } if (-not $tb.Focused) { [void]$tb.Focus() } }) $form.Add_Shown({ $form.Activate() [void]$tb.Focus() # Report BOTH pid and window handle: a proof caller resolving the handle via # Get-Process races with window creation and can read 0. Handle is stable # here because the form is realized by the time Shown fires. [System.IO.File]::WriteAllText($script:PidFile, "$PID`n" + $form.Handle.ToInt64()) }) $timer.Start() [void]$form.ShowDialog() $timer.Stop()