remote/tools/text_injection_proof.ps1

176 lines
7.6 KiB
PowerShell
Raw Normal View History

# 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 <utf16> -> 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 root derived from this script's location (tools/ is one level under the
# repo root) so the proof runs on any clone, not just the author's D: drive.
$repo = (Resolve-Path "$PSScriptRoot\..").Path
test(tools): 4a 横屏验收脚本 + 既有 proof 适配自动隐藏与同源缺陷修复 新增 landscape_proof.ps1:V-C1~C7 判据(自动横屏 <=2s、旋转零中断、 动画桌面下 >=45fps、控制条自动隐藏、黑边唤出零远端泄漏、四按钮可点、 方向四态循环、表单竖屏)。方向判定只解析 Display: mDisplayId=0 块内 cur=WxH——这台 vivo 无 mRotation= 行,且 dumpsys 里有常驻 1080x2400 的 虚拟屏,匹配首个 cur= 会在已横屏时误报未旋转(探针实测踩过)。 横屏自动隐藏与既有 proof 的判据自冲突(HUD 随控制条 GONE,RTT= 读不到 被误判断线/未连接),三个 proof 统一加「确认隐藏态才走黑边唤醒」: phone_chain_proof、ime_chinese_proof、rel_mouse_phone_proof。 修 rel_mouse_phone_proof 两处: - 2c 基线判据:横屏滑动 768px 的残余相对事件成批延迟落地,旧的「两次 读数相等」被突发间隙骗过(实测假失败 351/1418px)。改为 6 读数沉降 + 4 次采样判漂移 + 绝对泄漏点特征比对。 - step 4 靶窗启动缺 3 个 Mandatory 参数,子进程瞬间失败退出(3b 起一直 如此,右键落在裸屏幕中心,靠近违反 R-11「不得注入他人窗口」)。现按 契约启动并把指针停在自家窗口内。 R-5 同源排查:靶窗结果目录固定路径 + 启动时删自己的 stop.flag,会让 上一轮失败遗留的靶窗永远收不到退休信号、继续抢前台并写脏 received.txt (实测收到 abc\nabc)。5 个 proof 统一改每轮唯一目录(\47512 后缀)。 ime_chinese_proof 的切换键标定失效:设备 IME 已换成搜狗(原为讯飞), 旧坐标 (146,2096) 静默点空。IME 按键视图对 uiautomator 不可见、无法机械 标定,按 R-9 转人工确认,不硬凑自动化。
2026-11-01 08:58:19 -05:00
# UNIQUE per run: a fixed path lets the startup Remove-Item delete the stop.flag
# that leftover targets from a previous FAILED run are watching, so they never
# retire and keep writing into the shared received.txt
# (same trap fixed in phone_chain_proof, measured 2026-11-01).
$tmp = "$env:TEMP\remote-text-proof-$PID"
$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
feat(android): 键盘事件双通道 + 手机到 Windows 全链路 proof 真机暴露出 IME/按键通道的缺口,一并补上并证明: 1. RemoteInputConnection.sendKeyEvent 与 Activity.onKeyDown 都先处理**可打印** unicodeChar(走 v3 Text 通道),再退到扫描码查表。原先只查表:软键盘发的是 keyCode=UNKNOWN + unicodeChar,查不到表就被当未映射键吞掉,字整个丢了。 门槛排除控制值(Enter/Tab)——远端应用要的是按键事件而非 WM_CHAR。 2. onKeyDown/onKeyUp 补齐硬键与物理键盘路径(阶段 2 验收里的软键盘按键), repeatCount>0 只发一次 down,避免长按把远端消息队列刷爆。 isAutorepeat() 是 @hide,改用公开的 getRepeatCount()(javap 核实)。 3. getUnicodeChar() 返回的是单个 UTF-16 code unit 而非码点,注释按此改正: BMP 外字符由 IME commitText 成对送达,不经这条路。 tools/phone_chain_proof.ps1:手机按键 -> v3 Text -> agent SendInput -> 自有 Windows 靶窗自报内容,全链路一次跑通(真机 vivo S6 实测 abc 到位、ENTER 走 scan 28、BACK 断开回表单、表单 UI 重连成功)。要点: - 靶窗必须自报窗口句柄(pid+hwnd 两行),Get-Process 取 MainWindowHandle 有竞态; - 不能用 keybd_event(VK_MENU) 破前台锁:ALT 之后的首个字符被当菜单助记键吞掉, 实测三个字符只剩 b;改用 AttachThreadInput + SetForegroundWindow,不合成按键; - 退役遗留靶窗只能 touch 已存在的 stop.flag:新建会命中即将启动那次的停止路径, 靶窗刚起就自杀(实测 pid 报了又消失); - adb 进度信息走 stderr,PS 5.1 + ErrorActionPreference=Stop 会当终止错误, 按 build-android.ps1 同款办法临时放宽并只看退出码。 - 表单填字用 keyevent 而非 adb input text:设备只有第三方中文 IME,text 注入会被 组词(ASCII 句点变 U+3002)。跑前记录并禁用原 IME,finally 精确还原。
2026-10-30 00:58:36 -04:00
# target.pid holds "pid<n><hwnd>" (see text_target.ps1 Shown handler); take line 1.
$parts = @((Get-Content $pidFile -Raw) -split "`n")
$tid = [int]$parts[0].Trim()
$tp = Get-Process -Id $tid
Start-Sleep -Milliseconds 1200
feat(android): 键盘事件双通道 + 手机到 Windows 全链路 proof 真机暴露出 IME/按键通道的缺口,一并补上并证明: 1. RemoteInputConnection.sendKeyEvent 与 Activity.onKeyDown 都先处理**可打印** unicodeChar(走 v3 Text 通道),再退到扫描码查表。原先只查表:软键盘发的是 keyCode=UNKNOWN + unicodeChar,查不到表就被当未映射键吞掉,字整个丢了。 门槛排除控制值(Enter/Tab)——远端应用要的是按键事件而非 WM_CHAR。 2. onKeyDown/onKeyUp 补齐硬键与物理键盘路径(阶段 2 验收里的软键盘按键), repeatCount>0 只发一次 down,避免长按把远端消息队列刷爆。 isAutorepeat() 是 @hide,改用公开的 getRepeatCount()(javap 核实)。 3. getUnicodeChar() 返回的是单个 UTF-16 code unit 而非码点,注释按此改正: BMP 外字符由 IME commitText 成对送达,不经这条路。 tools/phone_chain_proof.ps1:手机按键 -> v3 Text -> agent SendInput -> 自有 Windows 靶窗自报内容,全链路一次跑通(真机 vivo S6 实测 abc 到位、ENTER 走 scan 28、BACK 断开回表单、表单 UI 重连成功)。要点: - 靶窗必须自报窗口句柄(pid+hwnd 两行),Get-Process 取 MainWindowHandle 有竞态; - 不能用 keybd_event(VK_MENU) 破前台锁:ALT 之后的首个字符被当菜单助记键吞掉, 实测三个字符只剩 b;改用 AttachThreadInput + SetForegroundWindow,不合成按键; - 退役遗留靶窗只能 touch 已存在的 stop.flag:新建会命中即将启动那次的停止路径, 靶窗刚起就自杀(实测 pid 报了又消失); - adb 进度信息走 stderr,PS 5.1 + ErrorActionPreference=Stop 会当终止错误, 按 build-android.ps1 同款办法临时放宽并只看退出码。 - 表单填字用 keyevent 而非 adb input text:设备只有第三方中文 IME,text 注入会被 组词(ASCII 句点变 U+3002)。跑前记录并禁用原 IME,finally 精确还原。
2026-10-30 00:58:36 -04:00
$hwnd = [IntPtr][long]$parts[1].Trim()
if ($hwnd -eq 0) { throw "target reported a null 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))] ==="