
在上一期中,我已经介绍了load(loadstring)的基本用法,这一篇笔记我继续详细介绍这个函数的一些用法技巧。

load(loadstring),这个函数对于大多数人来说,是很难接触到毫无卵用的,但是其实在罗技的lua环境里,这个函数其实偶尔也能可堪大用...
具体来讲,就是tm,罗技写API的团队就是个大草台班子,我真的服了..

注意到这个MoveMouseWheel,这个API就是模拟滚轮而已,非常简单的一个函数,你给它一个数,然后它就模拟滚轮滚几下,一瞬间就完成。
本来应该就是这样的,但是问题在于,如果你滚轮次数不确定,需要根据条件算出来,那你肯定就要写一个表达式对吧,类似这样:
MoveMouseWheel(a+b);
但是你这样一些就出大问题了,罗技一执行这东西直接就闪退了...
所以呢,后来我就想着是不是不能传入一个表达式,所以我就把上述代码改了一下变成这样:

这样用了一段时间,因为我用的是103.2.12(就是9.02.65,罗技这个版本号也是乱七八糟)的lgs,我这个版本的驱动支持这样的写法,但是后来又有人给我反馈,ghub这样写也会闪退,甚至在某些版本的lgs里,当这个参数算出来是0的时候,也会闪退...我真的是服了。
所以没办法的办法,只能使用loadstring强行拼一句代码出来了:

上述这个函数基本就可以实现所有版本的罗技都能支持(至少目前没再发现有意外的情况)。
其中if loadstring then 这一句,是出于兼容性考虑的写法,因为高版本罗技中使用的lua5.2以上版本,在lua5.2版本中loadstring被废除,使用load函数替代。
对于load(loadstring)这个函数,大家见到更常见的用法实际上是在各种所谓的加密当中,类似我上一篇笔记中提到的这样,这是由Ganlv提供的一种开源算法:

当然事实上,如果你认真理解了这个函数的话,那你就应该明白,事实上这样一种混淆手段,基本根本没有任何加密的效果,因为loadstring本质上只是把一串文本看成代码去运行而已。
我们将上述代码稍稍整理一下:

可以看到,loadstring的黄括号里一大堆,都是要传给loadstring的参数,loadstring返回然后再将运算结果输出给a,后面检查a是否被赋值,如果被赋值就执行a。
我们现在想获取加密前的源代码,完全可以直接把loadstring删掉,然后直接输出a就可以了(或者如果你的环境支持print,你可以直接把loadstring改成print):

我们把它放到罗技的脚本框里输出一下:

于是我们就获得了加密前的文本。
当然有时候也不是那么顺利,我们让他输出代码,它只给我们了一个□LuaQ(或者□LuaR、□LuaP类似的东西):

这其实就是因为它加密的源文件,并不是一个普通的lua文本文件,而是.luac——lua字节码文件。
所谓lua字节码文件,它由 Lua 解释器编译器(如 luac)生成,并由 Lua 虚拟机(Lua VM)直接执行,而无需再次解析源代码。很多人用这种文件,是希望进一步隐藏自己的源码。当然实际上受限于lua语言,luac还是非常容易被反编译,我们可以把a保存成文件,网上就有无数现成的软件甚至在线工具能反编译.luac文件,都不用自己逆向:

当然和绝大部分二进制文件一样,.luac文件也会丢失绝大部分的变量名,且代码写法会变动:

不过不管怎么说,我们还是获得了一个可编写更改的“源代码”。
至于最先开始我们输出的那个“□LuaQ”,这里额外做一个解释,我们可以用任意一个二进制编辑器打开.luac文件:

看到文件头部分,在小端模式下,.luac开头四个字节:0x61754c1b,这个其实就是.luac文件的“魔数(Magic Number)”。而第五个字节:0x51,指的是lua编译版本,我这里是lua5.1版本,于是就是0x51,0x51这个字节在ASCII里对应的字母是Q。最后第六位0x00,在字符串里表示字符串结束。于是和前面的魔数组合到一起,就变成了:“□LuaQ”(这个“□”符号,其实是因为0x1b在ASCII里表示的是Escape 字符,因为不是一个实际的字符,所以变成一个方块乱码)。
至于那个魔数,是指文件开头的特定字节序列,用于标识文件的类型。它通常占据文件的前几个字节,用于让操作系统或软件正确识别文件格式,而不依赖文件扩展名。最经典的就是Java.class里的那个CAFEBABE,然后还有PE文件的0x5a4d(MZ:MS-DOS开发者Mark Zbikowski)。
那么我们接下来做一件更刺激的事情,上面我们提到的内容,都是在知道文本密码的情况下输出源文件,那假设说我们没有这个密码,那我们能不能获取这个密码和源文件呢?我们试一下这个事情。
我们还是以Ganlv大佬的这个代码为例吧,不过他这个代码完全不是给人看的,我们稍稍整理一下这个代码:

这样这个代码的结构就清晰明了,下面是这个代码具体的逻辑:
1.loadstring编译的是匿名函数function(b,c)返回的结果,function(b,c)只有一句代码:return o(m(i(b), c));
2. return o(m(i(b), c))第一个运行i函数,它表明,传入的数组是被分成两块,前半块是用作指导重新还原后半块,后半块是被打乱顺序的数组,i函数将这个数组还原顺序并返回还原后的数组作为下一个函数m的第一个变量;

3.函数m将上个函数返回的数组与key循环按位异或:


4.最后o函数将结果拼成字符串作为最终结果返回:

到这里其实已经很明显了,这个加密方式对源代码做的处理,除了i函数的打乱顺序,完全就只有一个按位异或。
而我们知道对于异或存在以下规律:

因此如果我们知道一些脚本里肯定会出现的字符串,那我们就可以直接逆推出来key。
比如lua脚本里会大量出现的特征字符串,如function ,end;还有罗技lua专属的一些代码,如OutputLogMessage(,MoveMouseTo,GetMousePosition(等等。
我们一个一个字节遍历着去对比,然后筛选出来那些仅包含正常字符串的密码,然后我们就可以算出来原始密码了。这里只提供思路,具体代码实现虽然简单但是步骤比较繁琐,不方便展示这里不再赘述,有兴趣的读者可以尝试自己编写。
该系列笔记同步上传至百度网盘:
链接:https://pan.baidu.com/s/1W-SFxRl_6zThC5iPWEBgjw?pwd=61sm 提取码:61sm