起点:一堆积木,但积木盒是锁着的
手上有 26 个易语言(EPL)工程文件,时间从 2014-10 到 2019-08。都是学生时代的东西:聊天器、音乐播放器、QQ 机器人、端口映射、ARP 工具、远程控制、两个小整人程序、一个 PvZ 阳光修改器。
想归档发出来,卡在一个很实际的问题上:.e 是专有二进制容器,没有易语言 IDE 就"打不开"。而我不想为了读一堆旧代码去装 IDE,更不想为了发博客把源码先重写成别的语言。
但"打不开"和"读不了"是两件事。这篇讲怎么把 .e 读出来,以及读出来以后发现的、连我自己都不记得的东西。仓库地址在最后。
第一步:判断它是"存起来"还是"锁起来"
拿到一个陌生的二进制容器,第一个问题不是"格式规范在哪",而是:
里面的内容是被搬运过来的,还是被变换过的?
被搬运(plain storage)意味着你不需要理解格式就能拿到绝大部分信息;被变换(压缩/加密/编码)意味着你必须先解变换。
.e 属于前者。证据很直接:
00000000: 434e 5754 4550 5247 1973 1115 89cd 060a CNWTEPRG.s......
00000010: 1973 0002 bcb5 cfaa a3c5 cdbb c5ce 0219 .s..............
- 前 8 字节是魔数
CNWTEPRG,全仓 26 个文件都有,且只出现一次 —— 它是容器头,不是内容分隔符。 19 73反复出现(最终版服务端里出现 71 次),是某种记录/段标记。bcb5 cfaa a3c5 cdbb这种"高字节成对出现"的形态,就是 GBK 中文的字节特征。GBK 汉字是「首字节 0x81–0xFE + 尾字节 0x40–0x7E 或 0x80–0xFE」,看起来就是这种密密麻麻的乱码。
最硬的证据是搜一个必然存在的串。.e 里存着编译产物的完整路径,而这条路径在文件里是 GBK 编码的 —— 所以要拿 GBK 字节去撞,不能用你终端的 UTF-8:
>>> d = open('端口映射测试/服务端.源码.e', 'rb').read()
>>> d.count('端口映射'.encode('gbk')) # 撞 b'\xb6\xcb\xbf\xda\xd3\xb3\xc9\xe4'
1
明文可搜即说明没被变换。压缩过的数据不可能让你在中段搜到一个熟悉的字符串;加密过的更不会。(顺带一个坑:如果你的探针串里全是 ASCII,用 grep 直接搜就行;一旦含中文,就必须显式 .encode('gbk'),搜不到不代表不存在。)
这一条判断值得记住:拿到专有容器,先别去找格式文档,先做一个"明文探测"。花五分钟确定它是搬运还是变换,能决定后面两小时是扫字节还是去啃规范。
第二步:为什么 strings 一定会漏
strings 只找你默认字符集里连续的可打印 ASCII。也就是说它在这个小文件里只捞得出这种零碎:
d09f2340818511d396f6aaf844c7e325
krnln
EThread
全是库的标识符。而下面这些才是我真正想要的,它们一个都捞不出来:
_启动窗口
在程序启动后自动调入本窗口
服务器1
客户1
__启动窗口_创建完毕
系统核心支持库
中文常量、中文控件名、中文事件名在 .e 里全是 GBK 双字节,strings 会当成不可打印直接跳过。而易语言程序里几乎所有有意义的东西都是中文命名的:命令叫 取文本中间,事件叫 _按钮1_被单击,用户提示全是中文。所以 strings 在这类文件上的产出基本只有路径和几个英文 API 名,信息量约等于零。
结论:不能按"连续可打印 ASCII 区段"切,要按 GBK 的双字节规则逐字节走。
第三步:一个小扫描器
不到 30 行,够用:
import re
def gbk_runs(d, minlen=2):
"""返回 [(偏移, 文本)]:合法 GBK 解码后含 CJK,或 >=6 位 ASCII 的串。"""
out, i, n = [], 0, len(d)
while i < n:
start, chars = i, []
while i < n:
b = d[i]
if 0x81 <= b <= 0xFE and i + 1 < n: # GBK 首字节
t = d[i + 1]
if (0x40 <= t <= 0x7E) or (0x80 <= t <= 0xFE):
try:
chars.append(d[i:i + 2].decode('gbk'))
except UnicodeDecodeError:
break
i += 2
continue
break # 尾字节非法,断掉
if 0x20 <= b <= 0x7E: # 可打印 ASCII
chars.append(chr(b)); i += 1; continue
break
txt = ''.join(chars)
cjk = len(re.findall(r'[一-鿿 -〿-]', txt))
if cjk >= minlen or len(txt.strip()) >= 6:
if txt.strip():
out.append((start, txt.strip()))
if i == start: # 必须前进,防死循环
i += 1
return out
三个容易写错的点:
- 尾字节要分两段判断。
0x40–0x7E和0x80–0xFE,中间0x7F不是合法尾字节。写成一个连续区间会多出一堆假串。 - 单字符也可能解码失败。GBK 首尾字节合法的组合里仍有一片是未分配的,
decode必须包try,否则整段扫描抛异常。 if i == start: i += 1那行不是装饰。遇到非法字节时内层循环一次都不推进,外层就死循环。
拿到的是 (偏移, 文本) 对,偏移很重要 —— 后面所有定位、验证、脱敏都靠它。
第四步:长度前缀,实测
扫出来的串不是散落的,很多带 LE uint32 长度前缀(下面标的都是前缀自己的位置,正文从前缀之后开始)。在 1729 字节的小文件 端口映射测试/服务端.源码.e 里:
0x01dc 09 00 00 00 | 5f c6f4 b6af b4b0 bfda → 9 字节 "_启动窗口"
0x01e9 1a 00 00 00 | d4da ccd0f2 c6f4 b6af ba f3... → 26 字节 "在程序启动后自动调入本窗口"
0x03f3 0b 00 00 00 | b4b0 bfda b3cc d0f2 bcaf 31 → 11 字节 "窗口程序集1"
_启动窗口 = 1 + 2×4 = 9 字节,前缀正是 09 00 00 00;在程序启动后自动调入本窗口 13 个汉字 = 26 字节,前缀 1a = 26;窗口程序集1 = 2×5 + 1 = 11 字节,前缀 0b = 11。三个都对得上,前缀就是净字节数。
再往后有一条复合记录更有意思:
0x0386 39 00 00 00 → 57 字节
"krnln" 0d "d09f2340818511d396f6aaf844c7e325" 0d "5" 0d "0" 0d
"系统核心支持库" 00
6 + 1 + 32 + 1 + 1 + 1 + 1 + 14 + 1 = 57。拆开读就是「内核名 krnln / GUID / 版本 5.0 / 显示名 系统核心支持库」—— 这是一条支持库引用表项。krnln 不是乱码,它就是易语言核心库的文件名前缀,这一点在另一个文件里能直接印证:植物大战僵尸阳光修改器.e 的 0x17296E 处存着字面量 krnln.fnr。
所以这个容器是可读的,只是没有文档给你。你要做的是:找一条长度对得上总字节数的记录,把它按前缀切开,切完剩下的是零就基本说明你切对了。
第五步:一半的产出是垃圾
这是最该写下来的一段。扫描器直接给你的是候选集,不是结论。同一份输出里混着两类东西:
真信号:
0x01e0 _启动窗口
0x02a6 服务器1
0x03f7 窗口程序集1
0x058e __启动窗口_创建完毕
假阳性:
0x0014 嫉溪E突盼
0x016c 犆壕E位盼
0x2467 /P~/P~/P~/P~/Q
0x1850 /b3O凣k烱t㊣u甀v癕z砄{禟x疜w...
前两条是二进制结构里的随机字节"恰好"满足 GBK 合法性 —— 双字节编码的合法空间太大,随机数据里必然撞出一堆合法汉字。后两条是位图/图标资源区的像素游程,像素值刚好落在可打印 ASCII 上,于是 0x50 0x7e 0x50 0x7e... 被当成 “PPP~"。
我最后用的筛法,按信号强度排:
- 形状白名单。事件名一定长这样
^_{1,2}[\w\u4e00-\u9fff]_{1}[\w\u4e00-\u9fff]:_按钮1_被单击、__启动窗口_创建完毕。控件名是「中文词 + 数字」:编辑框按钮标签超级列表框服务器客户。支持库一定以支持库结尾。路径靠[A-Za-z]:\\和扩展名。网络靠 IPv4 正则、MAC、端口。 - 常见字占比。中文串里让常用汉字(的/了/是/在/文/件/号/端口/击/框/钮/标签/发送/服务…)占比 ≥0.45 才留下。真句子的常用字密度远高于随机字节撞出来的汉字 ——
嫉溪E突盼一个常用字都没有。 - 全角字符一票否决。假阳性里高频出现
E,这类全角字母和标点,因为随机字节组合很容易落在全角区。真内容里我几乎不用全角英文字母。 - 长度上限。超过 40 字节的中文串大概率是模块文档而不是我的逻辑。
顺序很重要:先用形状白名单捞出强信号,再用统计筛弱信号。反过来做会把真东西一起筛掉 —— 我第一版只上了常见字阈值,把 _按钮1_被单击 这类带下划线和数字的串全漏了。
第六步:长度前缀有个例外,而语句到底存不存
两个必须补的事实,都是先说错再改过来的。
前缀有时会把结尾 NUL 计进去
端口映射测试/服务端.源码.e 里那三个例子(9 / 26 / 28)算的是净字节数。但在 用热键来整人.e 里:
0x07f3 3f 00 00 00 → 63
"software\microsoft\windows\CurrentVersion\Run\UseHotKeyHaveFun" 00
└── 正文 62 字节 + 1 个 NUL = 63
0x1d89 08 00 00 00 → 8
"C:\1.exe" 00
└── 正文 7 + NUL = 8
同一个容器里,前缀一会儿是净长度、一会儿是净长度+1。不要拟合出一个统一规则然后到处套 —— 拿一条记录的字节数对不上,就去看它后面有没有 00,逐条确认。我之前在另一份归档里也差点被这个坑到:那个 SMTP 登录名 len=12,存的是 11 字符 + NUL,不是 12 字符的名字。
语句文本确实会留下来,但只留一两条
这是最颠覆预期的地方。易语言的表达式是以源码文本形态存的,运算符还是全角的 = +。我在 LP(用户)/LP聊天器.e 里捞到了这一行:
如果 (客户1.连接 (#IP, 20930) = 假)
这是我亲手敲的 如果 语句,原样躺在偏移 15032 处。#IP 是常量引用,同文件里存着它的值 127.0.0.1 —— 于是一行源码加一个常量,就还原出了"连本机 20930 端口"这个事实。
另一条在 LP远程遥控/服务器.e 偏移 5645:
_启动窗口.标题 = “对方已经关闭客户端。”
但别高兴太早。我把 26 个工程全跑了一遍,按"含全角 =/+ 且含中文"这条最像源码的规则统计:
| 文件 | 字节 | 扫出串 | 语句形态命中 | 其中真是我的代码 |
|---|---|---|---|---|
ARP攻击-易语言版.e | 1,136,159 | 32,005 | 15 | 0 |
帮啊吧弄得游戏.e | 2,108,215 | 83,980 | 9 | 0 |
植物大战僵尸阳光修改器.e | 1,755,148 | 75,198 | 4 | 0(全是随机字节) |
聊天器/客户端.e | 878,095 | 14,973 | 3 | 0(全是模块文档) |
聊天器/服务端.e | 680,722 | 13,731 | 3 | 0(全是模块文档) |
音乐播放器内测.e | 72,627 | 1,066 | 2 | 0(随机字节) |
LP(用户)/LP聊天器.e | 16,412 | 120 | 1 | 1 |
LP远程遥控/服务器.e | 7,434 | 73 | 1 | 1 |
| 其余 18 个工程 | — | — | 0 | 0 |
26 个工程、约 27 万条扫描串里,能确定是我亲手写的源码语句只有 2 行。
那 39 条命中分三类,每类都有可辨认的特征:
- 模块文档(
把文本递增1 编辑框1.内容 = 文本_递增 (编辑框1.内容)、可为空,默认即程序安装,参数 1=程序安装;2=程序卸载)。特征:语气是说明书,出现位置在文件的模块区(几十万字节往后),长度 20–200 字节,里面常有"返回"“参数"“默认为”。 - 随机字节撞出来的(
矟Ф櫎粫つ枾褱册櫞釁=o姂q唵専牷行滹稂)。特征:常见字占比接近 0,混着片假名和生僻字,全角算符出现在串中间而不是"左值 = 右值"的位置。 - 真源码。特征:出现在窗体定义之后不远的小偏移上,串里同时有你的控件名(
客户1、_启动窗口)和易语言关键字(如果、= 假)。
所以边界要说死:我这套字节扫描能捞到"某一行的原文”,捞不到"这段逻辑怎么组织”。语句没有顺序信息、没有嵌套、没有所属子程序,而且只在个别位置被存下来 —— 一个几百行的程序,容器里可能只留下 1 行。至于控制流到底能不能读出来,我在文末更正了一次:能,但不是靠我这套方法。
用它读 LP 聊天器,读出了什么
有了扫描器,剩下就是读。这块我从 2014-10 写到 2018-08,五个阶段:
| 时间 | 文件 | 状态 |
|---|---|---|
| 2014-10-29 | LP聊天器保存密码(服务端).e 3.4 KB / LP聊天器验证密码(服务端).e 2.6 KB | 两个各几十行的小服务端:一个把用户名密码明文写成文件,一个负责比对 |
| 2015-06-15 | LP(服务端)/LP聊天器服务器.e 8 KB | 第一个像样的服务端:_服务器1_数据到达、客户进入、客户离开,带一个发送框 |
| 2016-05-21 | LP(用户)/LP聊天器.e 16 KB | 客户端成型:注册、登录、断网重连,四个窗口程序集 |
| 2017-12-17 | 老版本/源码/{客户端,服务端}.e 20 KB / 22 KB | 中间版,已经有文件传输 |
| 2018-08-28 | 服务端.e 681 KB / 客户端.e 878 KB | 最终版 |
协议长什么样
服务端和客户端之间是纯文本行,命令名前缀一个 /。扫出来的客户端→服务端命令名(服务端、客户端两边各出现一次,成对):
/login 登录
/hqyzm 获取验证码(邮箱找回)
/changepassword 改密码
/deleteuser 注销用户
/returnping 回 ping
/returnfind 回"查找"结果
/gbckbt 关闭/重开标题?(这条我没读出全称)
服务端→客户端则是频道标签,且带完整文案:/xxk 出现 44 次、/jrwb 出现 26 次。看文案就能反推用途 —— /xxk 是"给这个用户的应答":
/xxk 注册成功!
/xxk 密码有误,请重试
/xxk 很抱歉,您的帐号在别处登录,您被迫下线。
/xxk 很抱歉,您的IP被冻结,如需解冻请联系开发者
/xxk 已经对您的邮箱发送了一封包含您密码的邮件,请查收。
/xxk Error!您所提交的信息不全,数据库中显示此帐号绑定了某个邮箱,请重试。
/jrwb 是"广播到所有文本框的系统消息":
/jrwb 系统:您的文件已经被 <某人> 所接收,您可以使用删除已上传文件按钮销毁文件。文号为:
/jrwb 系统:很抱歉,您不是文件上传者,无法删除文件。
/jrwb 系统:您的版本低于最新版本,现提示您更新。版本更新日志如下:
没有长度字段、没有编码、没有加密,就是 命令名 + 空格 + 明文负载。
版本演进也能从协议上看出来:2016-05 那版一个 / 命令都没有,纯灌文本;2017-12 那版只有 /jrwb(15 处);/xxk 是 2018-08 才出现的。也就是说我先有了"消息往框里写",过了半年才补上"这条消息该给谁看"。
服务端里还连着排了四个管理命令:
/banbf /banip /pingover /reload
旁边就是窗体上的 对IP发送指令、内部处理指令:、发送处理指令:、处理指令,以及一句 ping完成,共用时 —— 这是一个内建的运维面板:封 IP、封身份、测延时、重载配置。
身份字段叫「马甲」
客户端界面上写的是:
自选马甲:
自选马甲是你以后登录使用的东西,登录时使用自选马甲+密码进行登录。
自选马甲与密码都不能包含以下字符:
服务端侧有 文本替换_马甲 和模板占位符 %%马甲%%,以及两条 可使用Server,此处填写发出信息的马甲 / 可使用Server,此处填写收到信息的马甲。
所以「马甲」在这套系统里就是用户标识,而且服务端会做占位符替换 —— 这就是为什么 /jrwb 系统: 能伪装成系统发言。2016 年的我给自己的 IM 造了个 sender 覆盖机制,当时大概只觉得好玩。
文件传输用的是"文号"
上传文件 / 下载文件 / 删除已上传文件
选择发送对象:全体成员(all)
您可以让你的小伙伴使用文号下载。
很抱歉,您所提供的文号不正确,请核对后再试。
感谢您的使用,我们已经删除了您的信息
上传后服务端给一个短码(文号),接收方凭文号取文件,只有上传者能销毁。没有目录浏览、没有断点续传,就是一个一次性取件码。这大概是整个项目里最像"设计"的一处。
更新机制偷了个懒
检查更新 / 开始接收更新 / 现版本号 / 是否更新?
最新版本 记得修改
再加上扫到 网页属性_修改时间。组合起来就是:没有自建更新服务器,直接读远端文件的 HTTP Last-Modified 和本地 现版本号 比。一行服务端的活都没有,靠网页头搞定。
邮件找回密码
最终版服务端接了 SMTP:smtp.163.com,邮件正文是仿正规服务的口吻 ——
欢迎使用聊天器 / 感谢您的使用,验证码为 / 请勿回复本邮件 /
如非本人操作,请删除本邮件避免帐号被盗!
SMTP 的登录名和密码已经从归档里抹掉了(等长替换成 XXXX),主机名留着。
账号存储:一人一个 ini
\Data\用户信息\<用户名>.ini
不是数据库,就是一个用户一个文件。2014 年那个版本更直白:把用户名和密码明文写进两个文件里,一个叫 用户名、一个叫 密码。
隐藏控制台
点击此处让服务器后台运行
请使用Alt+P召回服务器
服务端可以缩成无界面跑,全局热键 Alt+P 唤回来。
878 KB 里有多少是我写的
几乎没有 —— 这是读出来以后最意外的结论。
看关键串的偏移分布(同一份代码在两个文件里的形态一致):
服务端.e 客户端.e
自己的协议文案 48k .. 105k —
SQLite 常量表 13630 .. 15613 25177 .. 27160
js_beautify 源码 15886 .. 18311 27433 .. 29858
汇编指令文档串 219568 .. 219578 417863 .. 417873
"精易" 相关 46069 .. 679076 262709 .. 876253
精易 那一条的跨度就说明问题了:客户端 878 KB,从 262709 往后基本全是精易模块(bbs.125.la 出的第三方模块集)的命令文档串和 API 表。里面甚至编进了 js_beautify 的 JavaScript 源码和一整张 SQLite 返回码常量表。模块的 .ec 绝对路径也原样留在容器里。
这是一条关于体积的教训:.e 会把用到的第三方模块原样编进容器。所以 1.1 MB 的 ARP 工具、1.75 MB 的 PvZ 修改器、878 KB 的聊天器,体积大头都不是我自己的代码。看易语言项目的文件大小去估计工作量,一定会得出荒谬的结论。
引用了别人的东西就写清楚是谁的 —— 模块里带着作者自己的注释 注:本来源于网络,只是整合到精易模块,方便大家使用,还有一处单独署名我一并保留了:
运算_十进制到十六进制,源码作者:fnsunbin@126.com
这行在 客户端.e 和 服务端.e 里都编进去了,我没有打码。
脱敏:只能等长替换
要发出去就得先把凭据抹掉。但 .e 是二进制容器,不能改长度:长度前缀是写死的,记录里还存着一堆绝对偏移,你把 11 字节改成 5 字节,后面整串的偏移全部错位,容器直接坏掉 —— IDE 打不开,我的扫描器也会开始输出乱码。
所以只有一种安全手法:等长字节替换。
替换表本身也是敏感物,所以下面只画形状、不写原值(<…> 里是类别;长度注释才是这段方法的全部信息量):
REPL = [
(b'<SMTP登录名>', b'XXXXXXXXXXX'), # 11 -> 11 登录名(连带发件地址一起消失)
(b'<SMTP密码>', b'XXXXXXXXXX'), # 10 -> 10 密码/授权码
(b'<QQ号>', b'XXXXXXXXXX'), # 10 -> 10 连带 @qq.com 形态
]
def redact(path):
d = open(path, 'rb').read(); n = 0
for old, new in REPL:
assert len(old) == len(new), (old, new) # 长度必须一致
n += d.count(old); d = d.replace(old, new)
return d, n, len(d)
加了三道锁:
- 每条替换
assert len(old) == len(new); - 写完立刻断言
len(new) == len(raw),整个文件字节数没变; - 推之前再扫一遍发布副本,把
gbk_runs跑在脱敏后的字节上,确认凭据 0 命中、XXXX在、fnsunbin@126.com还在。
第 3 条救了我一次。第一版脱敏表里我写的是完整邮箱(<登录名>@163.com),只匹配到了它 —— 而 SMTP 登录用的其实是不带域名的那半截,密码是另一条独立常量。两个都活着进了发布副本。是推之前的重扫把它们捞回来的。
教训:脱敏要扫凭据本体,不要扫凭据的展示形式。 邮箱、URL、日志行都是凭据的"外衣",容器里存的多半是裸值。
(QQ 号、老机器名、局域网 IP 按我自己的意思没打码,这些不构成风险;凭据类才清。)
顺带说:窗体记录是结构化文本,比逻辑好读得多
窗体定义区(一般在文件开头 10 KB 内)有一套自己的小序列化格式,每个控件一条记录,形状长这样:
0x039a 标签EX12 → 因特发送MAC → EX_UI- → &#UnicodeEX;宋体|
0x04a2 编辑框EX13 → └── 宋 = U+5B8B 宋
体 = U+4F53 体
&#UnicodeEX;&#<码点>; 是把属性值按码点转义存的,EX_UI- / UnicodeEX 是这一层的记录标记。上面那条解出来是控件 标签EX12、标题「因特发送MAC」、字体宋体。
认骨架比认格式有用。 你不需要知道 .e 的规范 —— 只要发现"每个已知为真的值(控件名、标题)前面都有固定长度的记录头、后面都跟着同一段 EX_UI-",就能反推出记录的边界和字段顺序。我判断窗体里有哪些控件、标题写什么、用什么字体,全靠这个,不是靠猜。
这套方法最后能走到哪
读得动的:窗体与控件名、控件标题与字体、事件名、所有用户可见文案(这些等价于一个 GUI 程序的协议文档和界面文档)、路径、IP/端口/MAC、支持库名与 GUID、第三方模块的作者注释,以及零星残留的源码行。
读不动的(对我这套方法而言):控制流。语句的存储是零星的、无顺序、无嵌套的(见上面那张 2 行 / 27 万串的表)。一个几百行的工程,容器里可能只留下 1 行原文。
要看逻辑,我当时以为只有两条路:装 IDE,或者把编译产物丢进 IDA/x64dbg 从机器码往上还原(后者对易语言基本是自虐 —— 它的运行时把每个命令都包成了对 krnln.fnr 的调用,控制流全在解释器里)。
这里是这篇发出去之后的一个更正:其实还有第三条路,而且我写这篇的时候它就已经存在。 有在维护的工具能把 .e 直接反编译成文本工程,控制流完整地回来 —— 我后来拿 e-packager(C++,MIT,win32 便携包)试了另一个项目:1.2 秒、零告警,.判断循环首、.如果真、.否则、嵌套、字符串字面量、属性赋值全在,产出是 src/*.txt + 窗体 XML + AGENTS.md。条件是给它一个易语言目录去加载 .fne 支持库,否则命令名会退化成 _Lib0Cmd36 这种占位符(结构照样解得出来,名字不行);同类工具还有 TextECode + QIQI.EProjectFile(C#,它连控制流的 AST 和反向 byteifier 都有,但 --allow-cleanup 默认开着,会删掉不是它生成的文件)。
所以那句话该怎么说:“命令序列和操作数是二进制记录,扫不出来"是对我这套字节扫描成立的描述,我当初把它当成对这个格式的描述发了出去。 只量长度依然有它的位置(便宜、不需要任何依赖、能拿到归属和哈希),但它不是"能不能读"的判据。这套方法真正的边界是:它回答"这里面有什么”,回答不了"它按什么顺序做"。
所以这个仓里的东西主要仍是存档 —— 我发它的时候它就是存档。发它的理由有两个:一是"我自己写的东西值得留个记录",二是"读专有二进制容器这个过程本身可复用"。真要说读到了什么逻辑,只有那套 /xxk、/jrwb 协议 —— 因为它的负载就是明文,常量表恰好等价于协议文档。
仓库
聊天器全部 8 个 .e(已脱敏)+ 演进说明:
https://cnb.cool/t1mmoc/tspace/eyuyan/lp-chat
同系列其他项目各一篇:ARP 欺骗、远程控制、音乐播放器、QQ 机器人、端口映射、小玩具两件、PvZ 阳光修改器。读 .e 的方法只在这篇讲,后面几篇直接引用。