# 2b V6/V7 proof: v3 text injection into a dedicated WinForms target window, # read back from that process itself. # ASCII-only source (PS 5.1 decodes BOM-less .ps1 as GBK; see personal lessons). # Non-ASCII payloads are built from explicit code points, never literals. # # Usage: # powershell -ExecutionPolicy Bypass -File tools\text_injection_proof.ps1 # # Chain: viewer-core text_check --units -> v3 agent SendInput # KEYEVENTF_UNICODE -> foreground WinForms TextBox -> self-reported text. # # Why NOT Notepad (2026-10-30): Windows 11 Notepad hosts all tabs in ONE # process and this dev box had a user document open in it - injecting "into # Notepad" would mean injecting into the user's real file. The WinForms target # is a separate process we own, opened on an empty field. # # Foreground handling (measured, not assumed): a form with TopMost=true that # is activated at startup KEEPS foreground for seconds; AppActivate / # SetForegroundWindow called from here actually STEALS it away. So we assert # the target is foreground before injecting, and refuse to inject otherwise - # a stray injection would land in whatever window was foreground. $ErrorActionPreference = "Stop" $repo = "D:\working\projects\yuankong\remote" $tmp = "$env:TEMP\remote-text-proof" $base = "D:\working\environment" $env:RUSTUP_HOME = "$base\rust" $env:CARGO_HOME = "$base\cargo" $env:PATH = "$base\mingw64\bin;$base\cargo\bin;" + $env:PATH Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; public static class WinFg { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); } "@ New-Item -ItemType Directory -Force -Path $tmp | Out-Null $pidFile = "$tmp\target.pid" $stopFile = "$tmp\stop.flag" $resultFile = "$tmp\received.txt" Remove-Item $pidFile,$stopFile,$resultFile -ErrorAction SilentlyContinue function Wait-File { param([string]$Path, [int]$TimeoutMs = 15000) $tries = 0 while (-not (Test-Path $Path) -and $tries -lt ($TimeoutMs / 150)) { Start-Sleep -Milliseconds 150; $tries++ } if (-not (Test-Path $Path)) { throw "file did not appear: $Path" } } function Read-Received { Wait-File $resultFile $bytes = [System.IO.File]::ReadAllBytes($resultFile) $text = [System.Text.Encoding]::UTF8.GetString($bytes) if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } return $text } function CodePointsToString([int[]]$cps) { -join ($cps | ForEach-Object { [char]$_ }) } function Show-Text([string]$s) { # ASCII-safe console preview: non-ASCII rendered as U+XXXX ($s.ToCharArray() | ForEach-Object { if ([int]$_ -ge 32 -and [int]$_ -lt 127) { $_ } else { "U+{0:X4}" -f [int]$_ } }) -join "" } function Assert-TargetForeground([IntPtr]$hwnd) { $fg = [WinFg]::GetForegroundWindow() if ($fg -ne $hwnd) { throw "foreground is $fg, not target $hwnd - refusing to inject (would hit another app)" } } function Send-Units([int[]]$cps, [IntPtr]$hwnd, [int]$delayMs = 1500) { $units = $cps -join "," $exe = "$repo\target\debug\examples\text_check.exe" if (-not (Test-Path $exe)) { throw "missing $exe (cargo build -p viewer-core --example text_check)" } Assert-TargetForeground $hwnd & $exe --port 47800 --code t --units $units --delay $delayMs --hold 800 $code = $LASTEXITCODE Assert-TargetForeground $hwnd # 注入窗口在 --delay 期间必须仍是靶窗 if ($code -ne 0) { throw "text_check exited $code" } Start-Sleep -Milliseconds 500 } Write-Host "=== 0. ensure CURRENT v3 agent is the one running ===" $ag = Get-Process agent -ErrorAction SilentlyContinue $agentExe = "$repo\target\release\agent.exe" if ($ag -and (Get-Item $agentExe).LastWriteTime -gt $ag.StartTime) { Write-Host " running agent predates current build; restarting" Stop-Process -Name agent -Force -ErrorAction SilentlyContinue Start-Sleep -Milliseconds 800 $ag = $null } if (-not $ag) { Start-Process -FilePath $agentExe -ArgumentList "--port","47800","--code","t" -WindowStyle Hidden Start-Sleep -Seconds 1 Write-Host " agent started (v3)" } Write-Host "=== 1. launch WinForms target (own process, empty field) ===" # NO -WindowStyle Hidden: the hidden host makes the form invisible, and an # invisible window can never be foreground (measured 2026-10-30). Start-Process powershell.exe -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass",` "-File","$repo\tools\text_target.ps1","-PidFile",$pidFile,` "-ResultFile",$resultFile,"-StopFile",$stopFile Wait-File $pidFile $tid = [int](Get-Content $pidFile -Raw).Trim() $tp = Get-Process -Id $tid Start-Sleep -Milliseconds 1200 $hwnd = $tp.MainWindowHandle if ($hwnd -eq 0) { throw "target has no window handle" } Write-Host " target pid=$tid hwnd=$hwnd title=[$($tp.MainWindowTitle)]" Assert-TargetForeground $hwnd Write-Host " target is foreground: yes" Write-Host "=== 2. V6: four CJK BMP characters (Ni Hao Shi Jie) ===" $cjk = @(0x4F60, 0x597D, 0x4E16, 0x754C) Send-Units $cjk $hwnd $got6 = Read-Received $exp6 = CodePointsToString $cjk Write-Host (" received [{0}] expected [{1}]" -f (Show-Text $got6), (Show-Text $exp6)) if ($got6 -ne $exp6) { throw "V6 FAIL" } Write-Host " V6 PASS - BMP CJK arrived intact through v3 Text + KEYEVENTF_UNICODE" Write-Host "=== 3. V7: exclaim + emoji U+1F600 (UTF-16 surrogate pair) appended ===" $mix = @(0x0021, 0xD83D, 0xDE00) Send-Units $mix $hwnd $got7 = Read-Received $exp7 = $exp6 + (CodePointsToString $mix) Write-Host (" received len={0} [{1}]" -f $got7.Length, (Show-Text $got7)) Write-Host (" expected len={0} [{1}]" -f $exp7.Length, (Show-Text $exp7)) if ($got7 -ne $exp7) { throw "V7 FAIL - mismatch" } if ($got7.Contains([char]0xFFFD)) { throw "V7 FAIL - U+FFFD present: pair got split" } Write-Host " V7 PASS - surrogate pair survived as one character, no replacement char" Write-Host "=== 4. negative guard: missing --units must NOT inject anything ===" $before = Read-Received $exe = "$repo\target\debug\examples\text_check.exe" # Omit --units entirely (passing an empty string would be eaten by PowerShell # argument handling and parsed as the next flag). viewer-core's parse_units # rejects the degenerate input before any session is even opened. # No 2>&1 here: viewer-core logs go to stderr by design; merging them turns # every normal log line into a PowerShell error record. & $exe --port 47800 --code t --delay 100 --hold 100 | Out-Null $emptyCode = $LASTEXITCODE Write-Host " exit=$emptyCode (expected non-zero: tool rejects empty --units)" if ($emptyCode -eq 0) { throw "FAIL - empty units should be rejected by the tool" } Start-Sleep -Milliseconds 400 $after = Read-Received if ($after -ne $before) { throw "FAIL - target text changed unexpectedly: [$after]" } Write-Host " guard PASS - no injection happened, target text unchanged" Write-Host "=== 5. stop target ===" New-Item -ItemType File -Force -Path $stopFile | Out-Null $tries = 0 while ((Get-Process -Id $tid -ErrorAction SilentlyContinue) -and $tries -lt 40) { Start-Sleep -Milliseconds 150; $tries++ } Write-Host "=== PROOF COMPLETE: final target text = [$(Show-Text (Read-Received))] ==="