起点:一堆积木,但积木盒是锁着的

手上有 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

三个容易写错的点:

  1. 尾字节要分两段判断。0x40–0x7E 和 0x80–0xFE,中间 0x7F 不是合法尾字节。写成一个连续区间会多出一堆假串。
  2. 单字符也可能解码失败。GBK 首尾字节合法的组合里仍有一片是未分配的,decode 必须包 try,否则整段扫描抛异常。
  3. 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攻击-易语言版.e1,136,15932,005150
帮啊吧弄得游戏.e2,108,21583,98090
植物大战僵尸阳光修改器.e1,755,14875,19840(全是随机字节)
聊天器/客户端.e878,09514,97330(全是模块文档)
聊天器/服务端.e680,72213,73130(全是模块文档)
音乐播放器内测.e72,6271,06620(随机字节)
LP(用户)/LP聊天器.e16,41212011
LP远程遥控/服务器.e7,4347311
其余 18 个工程——00

26 个工程、约 27 万条扫描串里,能确定是我亲手写的源码语句只有 2 行。

那 39 条命中分三类,每类都有可辨认的特征:

  • 模块文档(把文本递增1 编辑框1.内容 = 文本_递增 (编辑框1.内容)、可为空,默认即程序安装,参数 1=程序安装;2=程序卸载)。特征:语气是说明书,出现位置在文件的模块区(几十万字节往后),长度 20–200 字节,里面常有"返回"“参数"“默认为”。
  • 随机字节撞出来的(矟Ф櫎粫つ枾褱册櫞釁=o姂q唵専牷行滹稂)。特征:常见字占比接近 0,混着片假名和生僻字,全角算符出现在串中间而不是"左值 = 右值"的位置。
  • 真源码。特征:出现在窗体定义之后不远的小偏移上,串里同时有你的控件名(客户1、_启动窗口)和易语言关键字(如果、= 假)。

所以边界要说死:我这套字节扫描能捞到"某一行的原文”,捞不到"这段逻辑怎么组织”。语句没有顺序信息、没有嵌套、没有所属子程序,而且只在个别位置被存下来 —— 一个几百行的程序,容器里可能只留下 1 行。至于控制流到底能不能读出来,我在文末更正了一次:能,但不是靠我这套方法。


用它读 LP 聊天器,读出了什么

有了扫描器,剩下就是读。这块我从 2014-10 写到 2018-08,五个阶段:

时间文件状态
2014-10-29LP聊天器保存密码(服务端).e 3.4 KB / LP聊天器验证密码(服务端).e 2.6 KB两个各几十行的小服务端:一个把用户名密码明文写成文件,一个负责比对
2015-06-15LP(服务端)/LP聊天器服务器.e 8 KB第一个像样的服务端:_服务器1_数据到达、客户进入、客户离开,带一个发送框
2016-05-21LP(用户)/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)

加了三道锁:

  1. 每条替换 assert len(old) == len(new);
  2. 写完立刻断言 len(new) == len(raw),整个文件字节数没变;
  3. 推之前再扫一遍发布副本,把 gbk_runs 跑在脱敏后的字节上,确认凭据 0 命中、XXXX 在、fnsunbin@126.com 还在。

第 3 条救了我一次。第一版脱敏表里我写的是完整邮箱(<登录名>@163.com),只匹配到了它 —— 而 SMTP 登录用的其实是不带域名的那半截,密码是另一条独立常量。两个都活着进了发布副本。是推之前的重扫把它们捞回来的。

教训:脱敏要扫凭据本体,不要扫凭据的展示形式。 邮箱、URL、日志行都是凭据的"外衣",容器里存的多半是裸值。

(QQ 号、老机器名、局域网 IP 按我自己的意思没打码,这些不构成风险;凭据类才清。)


顺带说:窗体记录是结构化文本,比逻辑好读得多

窗体定义区(一般在文件开头 10 KB 内)有一套自己的小序列化格式,每个控件一条记录,形状长这样:

0x039a  标签EX12      →  因特发送MAC       →  EX_UI-  →  &#UnicodeEX;&#23435;&#20307;|
0x04a2  编辑框EX13    →                                    └── &#23435; = U+5B8B 宋
                                                                 &#20307; = 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 的方法只在这篇讲,后面几篇直接引用。