2016-10-02。两个文件:
服务器.e 7,434 字节
客户端.e 6,749 字节
这两个是归档里最适合做全文精读的样本 —— 小到没有编进任何第三方模块,把 gbk_runs 跑一遍,全部产出是 73 条 + 55 条。方法在上一篇。
当一个工程的扫描产出不到 100 条时,你不需要任何筛选技巧,直接把全部串打印出来读就行。这是我这次归档里学到的最实用的一条:研究格式要找最小的样本,写文章也是。
把两个窗口的串并排
服务器.e —— 73 条里的非噪声部分
480 _启动窗口
842 LP 远程控制 ---byXXXXXXXXXX ← 窗口标题(QQ 号已抹)
901 按钮2
1013 记录v
1045 服务器1 ← 监听控件
1167 标签5
1331 右边是图像
1377 标签4
1541 备注
1581 标签3
1745 代码
1918 鼠标类型
1966 按钮1
2078 开始执行
2247 类型 X坐标 Y坐标 ← 一行的格式
2467 写入 备注
2512 备注编辑框
2793 写入 代码
2838 代码编辑框
2955 图片框1
3270 系统核心支持库
3330 特殊功能支持库
3939 \Data\Data代码
4170 _图片框1_鼠标位置被移动
4248 横向位置
4269 纵向位置
4290 功能键状态
4399 鼠标位置
4487 _按钮1_被单击
4726 _按钮2_被单击
4905 _服务器1_客户进入
5075 有一个客户进入……
5111 _服务器1_数据到达
5321 __启动窗口_尺寸被改变
5567 _服务器1_客户离开
5645 _启动窗口.标题 = “对方已经关闭客户端。” ← 源码原文
5710 _图片框1_鼠标左键被按下
5939 鼠标类型 1
6029 _图片框1_鼠标右键被按下
6258 鼠标类型 3
6348 _标签5_鼠标左键被按下
6856 服务器.exe
客户端.e —— 55 条里的非噪声部分
794 请开始连接---远程客户端
849 时钟1
1073 断开连接l ← 末尾这个 l 不是我打的
1109 客户1 ← 连接控件
1385 第一个是IP第二个是端口
1555 开始连接
1591 编辑框2
1791 编辑框1
2082 系统核心支持库
2142 应用接口支持库 ← SetCursorPos 在这里
2160 EThread
2205 多线程支持库
2448 _按钮1_被单击
2693 正在连接中……
2751 _客户1_数据到达
3178 鼠标位置
3327 鼠标类型
3392 X坐标
3526 Y坐标
3776 文本类型
4118 您已经停止了远程遥控。
4278 _客户1_连接断开
4408 对方关闭了远程控制服务端。
4673 10.240.211.233 ← 编辑框1 的默认值(学校内网)
4705 连接服务器
4982 连接成功!等待对方执行指令
5252 连接失败,请更改设置再试。
5317 _时钟1_周期事件
5426 定时拍照 ← 关键的一条
5871 鼠标移动
5891 SetCursorPos
6205 客户端.exe
关于串尾那些多余的拉丁字母
断开连接l、找回密码n、顺序播放o、重新连接至服务器(断网重连)l、发送0、检查更新l —— 我这一批工程里到处是这种"末尾多一个 ASCII 字符"的串。
不是原作者打错,是我的扫描器越界,而且原因可以算出来。看 断开连接l 附近的原始字节:
1073 ← 08 00 00 00 │ b6cf bfaa c1ac bdb3 │ 6c 00 00 00 25 00 01 ...
长度前缀 8 └── 正文正好 8 字节 ──┘ └─ 下一条记录的头部 ─┘
「断开连接」= B6CF BFAA C1AC BDB3
字段是"长度前缀 + 正文",没有终止符。前缀说 8 字节,正文就到 1081 为止;而我的 gbk_runs 不认识前缀,它一路走到遇到非法字节才停,于是把下一条记录的头字节 0x6c(就是那个 l)也收了进去。后面 00 00 00 立刻截断了它,所以伪影永远只有 1–3 个字符。
校验方法:对任何可疑串,回看它前面的 uint32 前缀,用前缀算出正文应有的结尾,超出部分一律砍掉。上面那条 检查更新l(聊天器客户端.e 偏移 1523,前缀同样是 08 00 00 00)用这个办法一算就对上了。
我一开始差点把这些当笔误写进文章。读二进制容器时,任何"看起来很奇怪但极有规律"的现象,先怀疑自己的解析器,再怀疑原作者。
协议就这么被反推出来了
方向:名字起反了
服务器.e 里有 服务器1(监听控件),并且处理 _服务器1_客户进入;客户端.e 里有 客户1(连接控件),主动 连接服务器,界面上让你填 第一个是IP第二个是端口。
所以:
- “服务器” = 控制端(操作者,坐在自己机器前等连接)
- “客户端” = 被控端(要在目标机器上运行,并填入操作者的 IP)
这和常见的 RAT 恰好是同一个方向、不同的命名。被控端主动连出,所以能穿过目标机的入站限制 —— 这个我 2016 年当然不知道,只是照着"我操作 = 服务器"的直觉起的名。
画面怎么来的
控制端有个 图片框1 和一个标签 右边是图像,__启动窗口_尺寸被改变 让它跟着窗口缩放。但控制端没有任何截图相关的串。
被控端有:
5317 _时钟1_周期事件
5426 定时拍照
时钟周期事件里做的事叫定时拍照。所以画面是被控端按固定周期截屏、发回去、贴到 图片框1 上。没有增量、没有编码、没有压缩(两边都扫不到任何压缩或位图处理的串)—— 所以帧率和延迟大概都很难看。
输入怎么下去
被控端 _客户1_数据到达 里解析这几个字段:
鼠标位置 鼠标类型 X坐标 Y坐标 文本类型
然后 鼠标移动 事件调 SetCursorPos(来自应用接口支持库,这是唯一一个直接调 Win32 的地方)。
控制端侧更直白 —— 它把自己在图片框上的鼠标动作打上类型码:
_图片框1_鼠标左键被按下 → 鼠标类型 1
_图片框1_鼠标右键被按下 → 鼠标类型 3
_图片框1_鼠标位置被移动 → 横向位置 / 纵向位置 / 功能键状态
_标签5_鼠标左键被按下 → 同样取 横向位置 / 纵向位置 / 功能键状态
功能键状态 是被一起上报的,说明 Ctrl/Shift 这类修饰键有传;文本类型 说明还有文本通道(键盘输入或剪贴板,从串里分不出来)。
整个协议就是明文坐标流:鼠标类型 X坐标 Y坐标 三个数一行,没有长度字段、没有校验、没有鉴权。任何能连上端口的人都能控制那台机器。2016 年的我完全没意识到这件事,因为它的使用场景是"宿舍里两台机器互玩"。
那个没做完的"代码"通道
控制端还有一组东西:
按钮1 = 开始执行
代码编辑框 / 备注编辑框
写入 代码 / 写入 备注
类型 X坐标 Y坐标
记录v
\Data\Data代码
我的读法是:这是一个轨迹录制 + 回放。理由 —— 类型 X坐标 Y坐标 正好是"一行记录"的格式,记录v 像是记录表,写入 代码/写入 备注 是把内容落盘到 \Data\Data代码,代码编辑框 载入它,开始执行 按里面的行重放。备注 是给每段轨迹起的名字。
但也存在另一种可能:代码编辑框 里放的是可执行文本,被控端收到后解释执行 —— 那它就是这套东西里唯一"像后门"的部分。
我没法在容器里定死是哪种(控制流读不出来,见旗舰篇那张 2 行/27 万串的表)。所以我按第一种写,并在仓库 README 里保留了第二种可能。这种"两种解释都成立、证据不足以裁决"的状态,在归档旧代码时非常常见,承认它比硬选一个更有用。
全 26 个工程里只活下来 2 行源码,这是其中一行
偏移 5645:
_启动窗口.标题 = “对方已经关闭客户端。”
易语言把表达式以源码文本形态存了下来,运算符还是全角的 =。这一行的特征是"我的控件名 _启动窗口 + 全角算符 + 我的文案",三者齐备才敢认。另一行在 LP(用户)/LP聊天器.e:如果 (客户1.连接 (#IP, 20930) = 假)。
其余 24 个工程,0 行。
从这行还能读出一个设计选择:被控端下线时,控制端不是弹窗、不是状态栏,而是直接改自己的窗口标题。_服务器1_客户离开 → 标题变成"对方已经关闭客户端。"。很土,但一眼就看得见 —— 我当时的 UI 水平大概就是这样。
被控端侧对应的是 您已经停止了远程遥控。(_按钮2_被单击,也就是主动断开)和 对方关闭了远程控制服务端。(_客户1_连接断开)。两边都知道怎么优雅地告诉对方"没了",这在 2016 年算做得不错的错误处理。
仓库
https://cnb.cool/t1mmoc/tspace/eyuyan/lp-remote-control
服务器.e + 客户端.e。标题里那个 QQ 号在容器里已被等长替换,仓库里读出来就是 LP 远程控制 ---byXXXXXXXXXX,这里也不再重复原号。10.240.211.233 这个内网地址按我自己的意思留着 —— 2016 年学校网的机器,早就不存在了。
同系列还有一篇 ARP 欺骗,那篇的重点是"捞到的语句到底算谁写的"。