5.6 KiB
已 SSH 通道下解码 Windows 中文 UTF-16LE 输出
适用:已经通过
sshpass / ssh user@windows-host进了 Windows cmd 或 PowerShell,但终端里所有中文都成乱码(GBK 在 UTF-8 终端下被切碎成二进制)。
不要用chcp 65001—— Windows OpenSSH 默认把 channel 设置为 UTF-8,但 cmd 输出是 GBK,写到 pipe 后被 ssh 流程再次当 UTF-8 处理,结果是错的"二进制造纸"。
1. 症状(先把 ack 现象说清)
$ ssh user@host 'schtasks /query /tn "\Foo 任务" /v /fo LIST' 2>&1 | grep "LastRun"
grep: (standard input): binary file matches
# 中文 + 数字 + 标点的混合被 grep 视为 binary → -a 才能匹配
或者:
$ ssh user@host 'where hermes' 2>&1 | head
(空)
# cmd 输出看着是空,其实有内容,但都被当 binary 了
2. 三种解法(按推荐顺序)
解法 A:pipe 到 strings (推荐)
ssh user@host '<CMD>' 2>/dev/null | strings | grep -v "^$" | head -40
原理:strings(默认 GNU binutils)扫任何输入,把 ≥4 个字符的可打印字符串切出来,过滤掉控制字符。它对 UTF-8/GBK/UTF-16LE 全兼容,因为可打印字符在每种编码里都是 ASCII 范围。
例子:
$ sshpass -p xue.2538 ssh administrator@192.168.123.11 \
'schtasks /query /tn "\Hermes_Gateway" /v /fo LIST' \
2>/dev/null | strings | grep -iE "Last|Result|Command" | head -10
输出真实样例(这是 Windows Server 计划任务查询的真实响应):
: ZHOME\Administrator
: \Hermes_Gateway
: N/A
: ZHOME\Administrator
: C:\Users\Administrator\.hermes\gateway-service\Hermes_Gateway.cmd
注意第一列 : 之前是被 UTF-8 切碎的中文,全部丢了;剩下可打印的英文/数字/路径完整。
解法 B:grep -a 强制当文本(适合偶尔快速 grep)
ssh user@host '<CMD>' 2>/dev/null | grep -a -E "LastRun|Hermes"
grep -a(或 --binary-files=text)让 grep 不把 stdin 当 binary。坏处:中文输出会成 ,但你要找的英文 token(如 LastRun、Result、C:\...)都能匹配到。
解法 C:把输出重定向到文件再读回
如果目标 cmd 有大量中文,且 truly 中文也想看(比如 计划任务名、文件路径里有中文):
# 在 Windows 端重定向(cmd 用 GBK 重定向)
ssh user@host 'cmd /c "schtasks /query /tn \"\Hermes_Gateway\" /v /fo LIST > C:\Users\Administrator\task.txt 2>&1"'
# 拉回 Linux
scp user@host:/C:/Users/Administrator/task.txt /tmp/
# 用 iconv 把 GBK 转 UTF-8
iconv -f GBK -t UTF-8 /tmp/task.txt
但这只有在中文真的对解决问题必要时才做。绝大多数诊断只关心 ASCII token,能用解法 A 就别上 C。
3. 配套:把 Windows PowerShell 输出变干净
PowerShell 默认输出 UTF-16LE,传统 pipe 也会乱。两种干净做法:
用 -NoProfile -Command "<短命令>"
ssh user@host 'powershell -NoProfile -Command "Get-Service | Select Name,Status | ConvertTo-Csv -NoTypeInformation"' \
2>/dev/null | strings | head -10
或者输出 JSON 再 jq 解析
ssh user@host 'powershell -NoProfile -Command "Get-ScheduledTaskInfo -TaskName \"Hermes_Gateway\" -TaskPath \"\\\" | ConvertTo-Json"' \
2>/dev/null | strings | python3 -m json.tool
ConvertTo-Json 输出 UTF-8,ssh 不会切,但你要过 strings 兜底(仍然有些控制字符污染)。
4. 案例:解码 Windows 计划任务 LastRunTime 真正含义
PowerShell 提供 Get-ScheduledTaskInfo 才是真正的「上次运行时间 / 退出码」,不是 schtasks /query /v 拿到的那个 LastRun(那个常常显示系统时区不同的混乱格式)。
ssh user@host 'powershell -NoProfile -Command "Get-ScheduledTaskInfo -TaskName \"Hermes_Gateway\" -TaskPath \"\\\" | Format-List LastRunTime,LastTaskResult,NextRunTime,NumberOfMissedRuns"' \
2>/dev/null | strings | head -10
返回:
LastRunTime : 2026/7/1 18:26:26
LastTaskResult : 267014
NextRunTime :
NumberOfMissedRuns : 0
关键码表:
| LastTaskResult | 含义 |
|---|---|
| 0 | 操作成功完成 |
| 267011 | 任务尚未运行 |
| 267012 | 任务正在运行 |
| 267013 | 任务已禁用 |
| 267014 | 任务有要求,但任务尚未执行(开机启动项未触发场景) |
| 1058 | 显示检索服务不可用 |
| 1062 | 任务计划程序服务未运行 |
备注:267014 与 267011 经常被混用,都是 SCHED_S_TASK_HAS_NOT_RUN 家族;如果 LastRunTime 是
1999/11/30 0:00:00(Windows 默认占位值),再核对 NumberOfMissedRuns 是否 0 综合判断。
5. Linux 端 strings 不可用时的兜底
某些 Alpine/musl 容器没有 strings,用 tr -cd '\11\12\15\40-\176'(保留可打印 ASCII):
ssh user@host 'cmd' 2>/dev/null | tr -cd '\11\12\15\40-\176' | grep -E "Last|Run"
这个粗暴但能凑合。可打印 ASCII 范围 \40-\176 = 到 ~,足以抓到路径和英文 token。
6. 与 winserver-recon.md 的分工
- 侦察阶段(还没 SSH 通道)→
winserver-recon.md - 已 SSH 通道、处理 Windows 中文输出 → 本文件
- 服务状态/重启 →
hermes-debugSKILL.md 主体已有章节
7. 历史快照
- 2026-07-01:首次成型。背景:连接 winserver 后查计划任务,所有
schtasks /query /v输出都被 grep 当 binary;用strings兜底拿到LastTaskResult=267014/NumberOfMissedRuns=0健康判定。