这个系列前面几篇,读法只有一种:直接扫 .e 容器里的明文字符串,靠窗体名、控件名、字符串常量去反推程序在干什么。那份能推到底,是因为控制流读不出来。
这篇不一样。这份 2018-07-12 的工程(MC辅助程序.e,18,079 字节,一个程序集、两百多行源码,挂 iconv / shellEx / spec 三个支持库)我用 e-packager 解成了文本,.如果/.循环判断首/.计次循环尾 这些控制流是原样回来的。所以我不再"猜它做什么",可以直接讲它怎么做的、以及哪些做法能从易语言里搬走。
也先更正一句:我在前面几篇里写过"命令序列和操作数是二进制记录、扫不出来",那是对我那套字节扫描成立的描述,被我错当成对这个格式成立的结论发出去了。控制流是能读出来的——这条更正得认。
它本体是一台 Minecraft 联机的中间人:一边用 客户1 连着真实服务器,一边在本地开一个 服务器1,把游戏指向这个本地端口,两个方向的数据在中间来回搬。围绕这条中继挂了三样东西,下面一样一样拆。
一、找一个空闲本地端口:不问,直接试
要在本机开个监听把游戏流量接住,第一道坎是端口撞车。常见解法是问系统要一个空闲口,或者绑 0 让内核分配。这份代码两个都不用:
端口1 = 取随机数 (12914, 25565)
.循环判断首 ()
端口1 = 端口1 + 1
服务器1.端口 = 端口1
.循环判断尾 (客户2.连接 (“127.0.0.1”, 端口1) = 假)
客户2.断开连接 ()
返回 (“127.0.0.1:” + 到文本 (端口1))
拆开看这段循环在做什么:从 12914–25565 里随机挑一个起点,端口1+1、设给 服务器1,然后拿自己的第二个客户端 客户2 去连 127.0.0.1:端口1。连不上(返回假)就继续 端口1+1 再来一遍;一旦 客户2 连上了,循环判断尾 的条件不再成立,循环停在这个端口上,再断开探针 客户2,把 127.0.0.1:端口 吐回界面当映射地址。
这里有个易语言的写法顺带记一下:.循环判断尾 (条件) 是"满足条件就继续“的后测循环(do-while),所以退出靠的是"连上了"这件事本身让条件翻假,不用额外写 break,也不用先把端口表枚举一遍——整段找空闲口的逻辑就是这一个循环,没有第二处判断。
关键在于它证明端口可用的方式是当场演示一遍,而不是查询。要同时满足两件事:服务器1 真能在这个口上监听起来,并且真有个客户端连得上它。两个动作都发生在回环上,全程不碰任何真实数据;而"能不能连"这个返回值本身,就替代了所有权限和查询 API。
这套"用自连来确认资源可用"的思路,可迁移的地方比端口大得多:任何"找一个没人用的东西”(端口、临时文件名、一段 ID),只要它支持"占上+自测"这两个动作,就能把"查空闲"降级成"试一把",一个 if 都不用多写。
二、录制/回放:摸清一个陌生协议最便宜的入口
一台只会转发的中继其实没用;真正值钱的是它顺手把两个方向的包都存了下来。
先看转发本身,两段对称:
_服务器1_数据到达 → 客户1.发送数据 (服务器1.取回数据 ()) (游戏→远程,上行)
_客户1_数据到达 → 服务器1.发送数据 (目标, 客户1.取回数据 ()) (远程→游戏,下行)
下行的 目标 是在 _服务器1_客户进入 里 目标 = 服务器1.取回客户 () 记下来的那个本地连接。这两个事件里各还挂着一句:
.如果真 (选择框5.选中 且 单选框1.选中)
加入成员 (录制, 服务器1.取回数据 ())
选择框5 是录制总开关,单选框1/单选框2 决定录上行还是下行,录制 是个字节集数组。放出来在 _按钮3_被单击:把 录制[] 按原序整个再发一遍,选择框7(重复发送)勾上就一直循环整段,每循环一遍 延迟 (到整数 (编辑框8.内容)) 一拍;_按钮4 清空数组。注意延迟挂在"每一遍"上而不是"每一包"上——一包接一包原样倒出去,只有勾了循环时,两遍之间才停 编辑框8 那一拍。
整套中继还带自愈:_客户1_连接断开 里只要 选择框1 勾着,上行一断就自动重跑 _按钮1_被单击,把"连服务器+找端口"那一段整个再走一遍。挂着跑一整天不用人盯,这也是它敢当中间人的底气。
为什么这东西对"我根本不懂这个协议"的人是第一步——因为它不需要你理解任何一包。协议对你还是一个二进制黑盒时,你唯一确定的事实是这套字节序列是服务器刚刚接受的。把线上真实跑通的包原样录下来、再原样放回去,你就白捡了一份"保证可用"的样本集,不用查文档、不用反编译对端。这是"先让它自己把答案吐出来,再研究答案长什么样"的通用套路。
一旦有了录放,才谈得上实验:改间隔重放看时序依赖,动 录制 内容看有没有校验,对着明文流量做字节级观察永远比对着二进制包猜便宜。
(不过这份代码本身证明不了它对协议做了任何结构化理解——它没试图解码任何一个包。唯一一处"造包"在 发送 子程序里,那是作者自己手搓的一帧:02 03 01 连写三遍打头、末尾再补一个 02 03 01,中间两个十六进制长度字段 c = 成员数 + 2 和 c = 成员数 各带一套 < 14 / < 16 的补零约定,正文还先 编码转换 从 GBK 走一遍再逐字节转十六进制拼进去。两个长度各算各的、补零阈值还不一样,说明他在一条帧里塞了两个含义不同的长度值——但哪个字段对应协议里的什么,代码没给。这段和录放是两回事:录放负责"发现能用的包",手搓帧负责"照着某条格式套壳",套壳不等于看懂。)
三、≠ {2, 3, 1}:一条过滤暴露了一个标记包
上行转发那里还藏着一个判断。勾了 选择框6(界面上标题叫「自动去除空白」)时,转发前先过一道:
.如果真 (服务器1.取回数据 () ≠ {2, 3, 1})
客户1.发送数据 (服务器1.取回数据 ())
{2, 3, 1} 就是三个字节 0x02 0x03 0x01。也就是说:一个恰好长这三个字节的包,被当成"空白"直接丢掉、不往上转。
这是个很省事的取证点——被硬编码丢弃的固定字节序列,几乎一定是噪声类流量。作者给它起的名字叫"去除空白",说明在他眼里这三个字节根本不是数据,是水分。再结合 发送 里 02 03 01 被他当帧头帧尾反复使用,最合理的推断是这组字节在这个协议里是个定界/标记,{2, 3, 1} 多半是握手后连接维持期反复出现的那种 keepalive。
到此为止我能从代码里坐实的,只有"作者把 {2, 3, 1} 当噪声丢、又拿它当定界符用"。它到底是 keepalive 还是别的什么、对应 Minecraft 协议里的哪个包,是这段代码证明不了的——那条得靠对着真协议抓包,这里标成推断,不替他圆。
四、热键落在一个标签上:补上 2017 年那个洞
本系列有一篇收了两个整人小程序(lp-toys),其中一个 2017-12 的用热键来整人.e,我当时写得很直接:它整个窗口只有一个 标签1,能扫到的只有事件名 _标签1_反馈事件 和它的 参数一/参数二,没有 RegisterHotKey、没有虚拟键码、没有任何组合键文本——“它绑了什么组合键"读不出来。
这份 2018 的文件把这段机制原样摊开了:
.子程序 __启动窗口_创建完毕
F5 = 注册热键 (取窗口句柄 (), 标签1.取窗口句柄 (), 0, #F5键)
F6 = 注册热键 (取窗口句柄 (), 标签1.取窗口句柄 (), 0, #F6键)
.子程序 _标签1_反馈事件, 整数型
.参数 参数一, 整数型
.参数 参数二, 整数型
.如果 (参数一 = F5)
选择框5.选中 = 取反 (选择框5.选中)
.否则
.如果真 (参数一 = F6)
_按钮3_被单击 ()
.如果真结束
.如果结束
三件事一次讲清:热键在窗口创建完毕时注册;注册热键 的返回值就是热键 ID(存进 F5/F6 两个程序集变量);回调的落点是 标签1 的窗口句柄——第二个参数 标签1.取窗口句柄 () 告诉系统"热键消息发给这个标签”,参数一 装的就是被按下的那个热键 ID,所以拿它去比 F5/F6 就能分流:F5 翻录制开关,F6 直接触发重放。
机制就是:任何一个控件的窗口句柄都能被注册成全局热键的消息接收端,然后在那控件的"反馈事件"里按 ID 分发。 用谁当落点纯随手——这份用的就是标题写着「目标服务器IP」的 标签1,它甚至不是隐藏的,就是个正常标签兼职接消息。
这里只把机制说清楚,不认领那份 2017 程序绑的具体键。它注册了什么组合键、参数二 当时被用来做什么判断,还得解它自己那份才知道——机制通了不等于那份的值就回来了。
(顺带一个从这份解出来的文本里得到的通用认识:易语言把 Windows 的 WM_HOTKEY 包成了组件的 反馈事件,参数一 是热键 ID、参数二 是配套的整型参数。我那份 2017 文里猜"反馈事件八成来自扩展功能支持库一",这个说法现在有了一份带 注册热键 的同名事件样本可以对照,但仍不足以反推那份的绑定值。)
这个仓里有什么
仓库放的是这份 .e 本体,加上 e-packager 机器解出来的可读源码和窗体 XML(text/ 目录)。有件事要交代清楚:这类解包工具一般会在文本常量里插 #e2txt_text# 标记、给库命令加 krnln:: 命名空间前缀,这两种都是工具痕迹,不是易语言 IDE 里显示的样子——在别的解出来的工程里,你会看到字符串常量被写成 #e2txt_text# 紧贴内容前面、库命令被写成 krnln::运行 (...) 这种带命名空间前缀的样子,而 IDE 里只显示 运行。这份解出来两处痕迹都没有,控制流和字面量都是原样,所以仓里 text/ 那份可以直接当源码读,不用先去符号。
窗体默认值里还留着一个主机名 aXXXXXXXXXX.e2.luyouxia.net(蒲公英的内网穿透域名,前缀那串数字是当年的账号编号,这里等长打码)。这类 *.e2.luyouxia.net 域名是这几年的产物,和文件日期放一起,是个能给年代对账的旁证。
这份文件把三样东西接上了:一套能搬走的找端口和录放手法、一条能坐实的标记包取证点,还有 2017 年那篇留的那个"读不出来"的洞——它当时读不出来只是因为我手上只有字节扫描,不是热键机制本身有多神秘。