2026-10-30 00:09:48 -04:00
|
|
|
# 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,
|
2026-10-30 00:58:36 -04:00
|
|
|
[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
|
2026-10-30 00:09:48 -04:00
|
|
|
)
|
|
|
|
|
$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)
|
|
|
|
|
|
2026-10-30 00:58:36 -04:00
|
|
|
$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(); }
|
|
|
|
|
}
|
|
|
|
|
"@
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-30 00:09:48 -04:00
|
|
|
$timer = New-Object System.Windows.Forms.Timer
|
|
|
|
|
$timer.Interval = 150
|
|
|
|
|
$timer.Add_Tick({
|
|
|
|
|
if (Test-Path $script:StopFile) {
|
|
|
|
|
$form.Close()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-10-30 00:58:36 -04:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-30 00:09:48 -04:00
|
|
|
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()
|
2026-10-30 00:58:36 -04:00
|
|
|
# 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())
|
2026-10-30 00:09:48 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
$timer.Start()
|
|
|
|
|
[void]$form.ShowDialog()
|
|
|
|
|
$timer.Stop()
|