lp-toys 这个仓装了两个说不清用途的小东西:

鼠标跟随.e          7,211 字节   2016-04-07
用热键来整人.e      7,998 字节   2017-12-10

两个都没有编入第三方模块,扫描产出分别是 66 条和 32 条。小到不需要任何筛选技巧 —— 直接全贴出来读。方法本身在旗舰篇,这篇原来的重点是这套方法的天花板在哪。

写完之后我拿另一条路把这个天花板往上顶了一格(第三节),所以这篇现在有两部分:前面是字节扫描看到的清单,后面是同一份容器用反编译器解出来的真源码。中间那段"读不出来"的结论被推翻了,我没删,原样留着对照 —— 顺带把第一次写错的一处(C:\1.exe)也标出来。


一、鼠标跟随.e:清单就把意图说完了

   480  _启动窗口
   493  在程序启动后自动调入本窗口
   818  时钟1
   930  按钮3
  1042  停止跟随
  1078  按钮2
  1190  开始跟随
  1226  按钮1
  1338  添加图片
  1374  通用对话框1
  1540  图片格式(*.JPG;*.GMP)
  1605  动画框1
  1769  图片1
  1985  系统核心支持库
  2009  扩展界面支持库二
  2111  多线程支持库
  2223  窗口程序集1
  2297  图片
  2379  __启动窗口_尺寸被改变
  3199  _按钮1_被单击
  3915  _按钮2_被单击
  4040  _按钮3_被单击
  4165  线程
  5042  _动画框1_动画框鼠标位置改变
  5124  横坐标
  5143  纵坐标
  5162  已被按下物体
  5817  _时钟1_周期事件
  5961  图片组
  6087  路径
  6125  执行图片
  6150  标识值
  6169  鼠标水平位置
  6194  鼠标垂直位置
  6219  鼠标横类型
  6242  鼠标纵类型
  6508  鼠标跟随.exe

程序意图完全透明:

  • 添加图片(按钮1)→ 通用对话框1 选文件 → 存进 图片组(记住 路径、标识值)
  • 开始跟随(按钮2)/ 停止跟随(按钮3)控制 时钟1
  • _时钟1_周期事件 里读 鼠标水平位置 / 鼠标垂直位置,把 执行图片 画到 动画框1 上
  • __启动窗口_尺寸被改变 让动画框跟着窗口缩放
  • _动画框1_动画框鼠标位置改变 + 横坐标 / 纵坐标 / 已被按下物体 是动画框自己的交互事件

两份清单我都按长度前缀截干净了。原始输出里还带着远程控制那篇说的 1–3 字节越界伪影 —— 比如这里实际扫到的是 系统核心支持库;、扩展界面支持库二9、多线程支持库8,末尾那个字符属于下一条记录。

一个"自定义光标"玩具。用的是扩展界面支持库二(动画框 只在这个库里),所以它必须挂这个库 —— 这类"控件名 → 属于哪个支持库"的对应关系,扫一遍就能反过来确认我没读错。

那个 *.GMP

图片格式(*.JPG;*.GMP)

GMP 不是任何图像格式。应该是 GIF 打错了 —— 而 *.JPG 也写成大写在易语言的过滤器里很常见(它按字面显示,不影响匹配)。

这种小错是最能证明"这真是我自己写的"的东西。我留着它,没改。


二、用热键来整人.e:能枚举,不等于能读懂

这个程序的全部扫描产出:

   480  _启动窗口
   493  在程序启动后自动调入本窗口
   778  标签1
  1022  d09f2340818511d396f6aaf844c7e325     ← 系统核心支持库 GUID
  1059  系统核心支持库
  1077  shellEx                              ← 支持库的内核名
  1085  DA19AC3ADD2F4121AAD84AC5FBCAFC71     ← shellEx 的 GUID
  1122  扩展功能支持库一
  1178  窗口程序集1
  1400  Locked                               ← 标识符(后来知道是逻辑型程序集变量)
  1419  ShutDown                             ← 同上
  1758  __启动窗口_创建完毕
  3358  _标签1_反馈事件                      ← 唯一的事件处理函数
  3420  参数一
  3439  参数二
  4899  shutdown /s /t 60
  5031  shutdown /a
  5613  By:XXXXXXXXXX@qq.com                 ← 作者署名里的 QQ 号已被等长替换
  7509  庆郓D                                ← 伪影
  7561  C:\1.exe

窗口上没有按钮、没有编辑框,只有一个 标签1。界面几乎是空的 —— 它不需要界面。

读得出的行为画像

四条独立证据,可以拼出一个完整功能:

证据含义
shutdown /s /t 6060 秒后关机。倒计时留了 60 秒,是给受害者抢救用的
shutdown /a取消关机 —— 程序知道怎么撤销自己干的事
shellEx 支持库扫到的配对是 shellEx ↔ 扩展功能支持库一(内核名和显示名挨着存),这个库提供的就是执行 shell 命令的能力,两条 shutdown 都走它
software\microsoft\windows\CurrentVersion\Run\UseHotKeyHaveFun注册表 Run 项自启动,值名直译"用热键找乐子"
C:\1.exe我当时把它读成"它把自己复制到 C:\1.exe"——这条是错的,第四节讲它到底是什么
Locked / ShutDown两条挨着存的标识符。我按字面当成两种玩法的名字,但值没读出来。它们其实是逻辑型程序集变量,不是常量

二、第三条路:同一个容器,换个工具就解出源码了

上面那节写完之后,我在另一批旧工程上试成了 e-packager(MIT,一个把易语言 .e 直接解成文本源码的工具),于是回头把这两个容器又解了一遍。为了不碰原始档,我解的就是这个仓里那份已经脱敏的发布副本 —— 它照样能解,只有被打码的那条字符串跟着变。

先说它把什么读出来了,再说为什么字节扫描读不出来。

为什么那 32 条串里没有热键

我把容器按字节复核了一遍,数了数几个词出现的次数:

找的东西容器内 GBK 字节出现次数
注册热键 / 运行 / 信息框 / 写注册项 / 取窗口句柄0
#Alt键状态 / #L键 / #NumLock键0
Locked / ShutDown各 1
标签12
shutdown /s /t 60 / shutdown /a各 1 —— 这两条是手打的字符串常量,属于"会落明文"的那一类

规则就出来了,而且很干净:

.e 容器里明文存的,只有你自己起的名字和你手打的字符串。平台词汇不在里面。

命令名(注册热键)、修饰键和键位常量(#Alt键状态、#L键)都是编译期/加载期从支持库里按序号解析的,容器里只留序号不留文本。所以"读不出热键"不是运气差,是这类信息按格式就不落明文。而窗体名、子程序名、变量名、字符串常量是用户产生的,一律明文。这一条比"控制流在二进制里"精确得多,我可以据此预判任何一个 .e 里什么能扫到什么扫不到。

它到底绑了什么键

真源码里 __启动窗口_创建完毕 一共调了 12 次 注册热键,落点全是 标签1.取窗口句柄 ():

a  = 注册热键 (取窗口句柄 (), 标签1.取窗口句柄 (), #Alt键状态, #L键)
b  = 注册热键 (..., #Alt键状态, #S键)
c  = 注册热键 (..., #Alt键状态, #D键)
d  = 注册热键 (..., #Alt键状态, #A键)
e  = 注册热键 (..., #Alt键状态, #E键)
f  = 注册热键 (..., #Alt键状态, #B键)
g  = 注册热键 (..., #Alt键状态, #Y键)
l2 = 注册热键 (..., #Alt键状态, #O键)
l3 = 注册热键 (..., #Alt键状态, #G键)
l4 = 注册热键 (..., #Alt键状态, #I键)
l5 = 注册热键 (..., #Alt键状态, #N键)
l6 = 注册热键 (..., 0,          #NumLock键)

(同一个机制在这批另一篇里被完整走通过一遍:注册热键 的第二个参数要一个窗口句柄当回调落点,他就随手抓了个标签控件的句柄,回调里靠 参数一 比对热键 ID 分流。所以 _标签1_反馈事件 这个在标准标签事件表里对不上号的名字,出处也一并清楚了。)

_标签1_反馈事件 里的分发是一棵一百多行的 .如果 嵌套(整个程序集才 179 行,它占了 134 行),读下来是这样一张表:

组合键干什么
Alt+O → Alt+G → Alt+I → Alt+N一条四步解锁序列,按对每一步置一个旗标,按错任意一步全部旗标清零、从头再来
NumLock上面四步走完之后按它,led 置真 —— 整套整人功能这才通电
Alt+L切 Locked。程序启动时 Locked = 真,也就是默认锁着,通电之后还得再解一次锁
Alt+S切 ShutDown(待命旗标)
Alt+D只在 ShutDown = 真 时生效:运行 ("shutdown /s /t 60", ...),然后把待命旗标按回去
Alt+A运行 ("shutdown /a", ...) 撤销关机,无条件生效
Alt+E总复位:led、la…le 全置假、Locked 置回真 —— 想收工得按这个
Alt+B切 BY
Alt+Y只在 BY = 真 时弹一个信息框,正文是 By:XXXXXXXXXX@qq.com(QQ 号已打码),标题写着 2333

也就是说,我第一版猜的"累计按了几次才关机"方向对了一半:确实有计数,但那个计数是解锁序列,而且它和关机那条链是隔开两层的 —— 先走序列、再按 NumLock 通电、再 Alt+L 解 Locked,之后 Alt+S 待命、Alt+D 才真的挂上 60 秒倒计时。四个键 O G I N 是序列本身,界面上一个字都没写,忘了就只能重来。

一个 8 KB 的整人程序,做了一套两段式 arm 流程和一个只有作者本人知道的四键序列。这比我"读不出来"时猜的还要上心。

顺带:鼠标跟随 的控制流也读出来了

同一个工具解的。它的"跟随"不是每张图各自跟随:开始跟随 只是把 时钟1.时钟周期 = 100,而这个时钟每响一次就 启动线程 (&线程, , ) 起一个新线程、且不保存句柄;线程 里用 延迟 (图片.执行图片 × 10) 按序号错峰,再把每张图摆到 鼠标位置 - (序号-1)×10 - 方向补偿 上。那个方向补偿是 80 或 0,由 _动画框1_动画框鼠标位置改变 里比较新旧坐标算出来 —— 往右移就整体往左让 80 像素,拖尾才出现在运动的反方向。停止跟随 是把周期设回 0,已经在飞的线程一个都没管。


四、C:\1.exe 那条是我读错了

第一版的行为画像里有一行:「自启动项要指向一个稳定路径 —— 它把自己复制到 C:\1.exe」。这个程序里根本没有复制动作。 真源码全文跟文件有关的只有一句:

写注册项 (#本地机器,
    "software\microsoft\windows\CurrentVersion\Run\UseHotKeyHaveFun",
    取运行目录 () + "\" + 取执行文件名 ())

它注册的是**“我这次是从哪个目录跑的”**,不复制、不落地、不改名。

那 C:\1.exe 是哪来的?我把解包输出的每个文件都搜了一遍,1.exe 0 命中 —— 它压根不在源码文本里。回容器里看它自己那条:位于偏移 7,561,前面挂的 LE uint32 正好是 8,而 C:\1.exe 恰好 8 个字节,是条合法的长度前缀串;它周围是容器尾部那批 19 73 记录,也就是工程级信息待的地方。最合理的解释:这是他当初把 exe 直接编译到 C 盘根目录时留下的编译输出路径,和这批别的工程里那些 D:\...\xxx.exe 构建路径是同一类记录。

所以真实情况比我写的更省事也更糙:他把产物直接扔到 C:\1.exe 跑了一次,Run 项记下的是"当时所在的那个目录 + 当时的文件名"。你把 exe 挪走,下次它自己从新位置重跑一遍注册表就跟着改;你把 exe 删了,那条 Run 项就留在 HKCU 里指向一个不存在的文件 —— 它不会自愈,也不需要自愈,因为它本来就没打算藏住自己。

这一条错得很有价值,值得单独留一节。 字节扫描读出来的字符串,“是代码里的字面量"还是"是工程元数据”,长得一模一样:都明文、都带长度前缀、都在同一个容器里。我把后者当成了前者,于是凭空给一个程序加了一个自我复制的行为,还顺手编了一套"这是我对持久化的理解"。判据也简单,而且便宜:

解一次包,看这条字符串落不落在 src/ 里。落不进去的,就不是代码。

哈希、偏移、字节数这些我可以随便贴,但归属层才是决定它能不能当证据的东西。


为什么这两个合在一个仓里

两个都是不到 100 条串的小东西,各开一个仓没意义。而且它们唯一的共同点就是"做完就为了给人看两秒"。

发它们的理由也不是技术,是那两个文件名。鼠标跟随 和 用热键来整人 —— 一个 2016 年,一个 2017 年,中间隔着我做聊天器和机器人的那段时间。那阵子我写代码的全部动机是"我想让屏幕上出现一个不该出现的东西"。这个动机后来没再出现过,值得留个证据。

(QQ 号我这次统一抹掉了 —— 容器抹了,正文里也不再印一遍。同系列的远程控制那篇里,标题尾巴上的 by… 同样是打码后的形状。)


仓库

https://cnb.cool/t1mmoc/tspace/eyuyan/lp-toys